Làm cách nào để kết hợp hai hoặc nhiều truy vấn trong chế độ xem Django?


653

Tôi đang cố gắng xây dựng tìm kiếm cho một trang web Django mà tôi đang xây dựng và trong tìm kiếm đó, tôi đang tìm kiếm theo 3 mô hình khác nhau. Và để có được phân trang trên danh sách kết quả tìm kiếm, tôi muốn sử dụng chế độ xem object_list chung để hiển thị kết quả. Nhưng để làm điều đó, tôi phải hợp nhất 3 truy vấn thành một.

Làm thế nào tôi có thể làm điều đó? Tôi đã thử điều này:

result_list = []            
page_list = Page.objects.filter(
    Q(title__icontains=cleaned_search_term) | 
    Q(body__icontains=cleaned_search_term))
article_list = Article.objects.filter(
    Q(title__icontains=cleaned_search_term) | 
    Q(body__icontains=cleaned_search_term) | 
    Q(tags__icontains=cleaned_search_term))
post_list = Post.objects.filter(
    Q(title__icontains=cleaned_search_term) | 
    Q(body__icontains=cleaned_search_term) | 
    Q(tags__icontains=cleaned_search_term))

for x in page_list:
    result_list.append(x)
for x in article_list:
    result_list.append(x)
for x in post_list:
    result_list.append(x)

return object_list(
    request, 
    queryset=result_list, 
    template_object_name='result',
    paginate_by=10, 
    extra_context={
        'search_term': search_term},
    template_name="search/result_list.html")

Nhưng điều này không làm việc. Tôi gặp lỗi khi tôi cố gắng sử dụng danh sách đó trong chế độ xem chung. Danh sách bị thiếu thuộc tính clone.

Có ai biết làm thế nào tôi có thể hợp nhất ba danh sách page_list, article_listpost_list?


Có vẻ như t_rybik đã tạo ra một giải pháp toàn diện tại djangosnippets.org/snippets/1933
akaihola

Để tìm kiếm, tốt hơn là sử dụng các giải pháp chuyên dụng như Haystack - nó rất linh hoạt.
người hướng dẫn

1
Người dùng Django 1.11 và abv, xem câu trả lời này - stackoverflow.com/a/42186970/6003362
Sahil Agarwal

lưu ý : câu hỏi được giới hạn trong trường hợp rất hiếm khi sau khi hợp nhất 3 mô hình khác nhau lại với nhau, bạn không cần phải trích xuất lại các mô hình trên danh sách để phân biệt dữ liệu về các loại. Đối với hầu hết các trường hợp - nếu dự kiến ​​phân biệt - nó sẽ sai giao diện. Đối với các mô hình tương tự: xem câu trả lời về union.
Sławomir Lenart

Câu trả lời:


1058

Ghép các truy vấn vào một danh sách là cách tiếp cận đơn giản nhất. Nếu cơ sở dữ liệu sẽ được đánh cho tất cả các truy vấn (ví dụ: vì kết quả cần được sắp xếp), điều này sẽ không thêm chi phí.

from itertools import chain
result_list = list(chain(page_list, article_list, post_list))

Việc sử dụng itertools.chainnhanh hơn việc lặp từng danh sách và nối từng phần tử một, vì itertoolsđược triển khai trong C. Nó cũng tiêu tốn ít bộ nhớ hơn so với chuyển đổi từng bộ truy vấn thành một danh sách trước khi nối.

Bây giờ có thể sắp xếp danh sách kết quả, ví dụ theo ngày (như được yêu cầu trong bình luận của hasen j cho câu trả lời khác). Các sorted()chức năng thuận tiện chấp nhận một máy phát điện và trả về một danh sách:

result_list = sorted(
    chain(page_list, article_list, post_list),
    key=lambda instance: instance.date_created)

Nếu bạn đang sử dụng Python 2.4 trở lên, bạn có thể sử dụng attrgetterthay vì lambda. Tôi nhớ đã đọc về nó nhanh hơn, nhưng tôi không thấy sự khác biệt đáng chú ý về tốc độ cho danh sách hàng triệu mặt hàng.

from operator import attrgetter
result_list = sorted(
    chain(page_list, article_list, post_list),
    key=attrgetter('date_created'))

13
Nếu hợp nhất các truy vấn từ cùng một bảng để thực hiện truy vấn OR và có các hàng trùng lặp, bạn có thể loại bỏ chúng bằng chức năng nhóm: from itertools import groupby unique_results = [rows.next() for (key, rows) in groupby(result_list, key=lambda obj: obj.id)]
Josh Russo

1
Ok, vậy thì về chức năng nhóm trong bối cảnh này. Với chức năng Q, bạn sẽ có thể thực hiện bất kỳ truy vấn HOẶC nào bạn cần: https://docs.djangoproject.com/en/1.3/topics/db/queries/#complex-lookups-with-q-objects
Josh Russo

2
Chuỗi @apelliciari sử dụng bộ nhớ ít hơn đáng kể so với list.extend, vì không cần tải cả hai danh sách vào bộ nhớ.
Dan Gayle

2
@AWrightIV Đây là phiên bản mới của liên kết đó: docs.djangoproject.com/en/1.8/topics/db/queries/ chủ
Josh Russo

1
dùng thử phương thức này nhưng có'list' object has no attribute 'complex_filter'
Grillazz

466

Thử cái này:

matches = pages | articles | posts

Nó giữ lại tất cả các chức năng của các truy vấn đó là tốt đẹp nếu bạn muốn order_byhoặc tương tự.

Xin lưu ý: điều này không hoạt động trên các truy vấn từ hai mô hình khác nhau.


10
Mặc dù vậy, không hoạt động trên các truy vấn cắt lát. Hay tôi đang thiếu một cái gì đó?
sthzg

1
Tôi đã từng tham gia các truy vấn bằng cách sử dụng "|" nhưng không phải lúc nào cũng hoạt động tốt Tốt hơn là sử dụng "Q": docs.djangoproject.com/en/dev/topics/db/queries/ Kẻ
Ignacio Pérez

1
Nó dường như không tạo ra các bản sao, sử dụng Django 1.6.
Teekin

15
Đây |là toán tử hợp nhất được thiết lập, không phải bit OR.
e100

6
@ e100 không, đó không phải là toán tử hợp nhất. django quá tải toán tử bitwise OR: github.com/django/django/blob/master/django/db/models/
trộm

109

Liên quan, để trộn các truy vấn từ cùng một mô hình hoặc cho các trường tương tự từ một vài mô hình, Bắt đầu với Django 1.11, một qs.union()phương thức cũng có sẵn:

union()

union(*other_qs, all=False)

Mới trong Django 1.11 . Sử dụng toán tử UNION của SQL để kết hợp các kết quả của hai hoặc nhiều Truy vấn. Ví dụ:

>>> qs1.union(qs2, qs3)

Toán tử UNION chỉ chọn các giá trị riêng biệt theo mặc định. Để cho phép các giá trị trùng lặp, sử dụng đối số all = True.

Các trường hợp mô hình trả về union (), ngã tư () và sự khác biệt () của loại Truy vấn đầu tiên ngay cả khi các đối số là Truy vấn của các mô hình khác. Truyền các mô hình khác nhau hoạt động miễn là danh sách CHỌN giống nhau trong tất cả các Truy vấn (ít nhất là các loại, các tên không quan trọng miễn là các loại theo cùng một thứ tự).

Ngoài ra, chỉ cho phép LIMIT, OFFSET và ORDER BY (tức là cắt và order_by ()) trên Truy vấn kết quả. Hơn nữa, cơ sở dữ liệu đặt ra các hạn chế về những hoạt động được phép trong các truy vấn kết hợp. Ví dụ: hầu hết các cơ sở dữ liệu không cho phép LIMIT hoặc OFFSET trong các truy vấn kết hợp.

https://docs.djangoproject.com/en/1.11/ref/models/querysets/#django.db.models.query.QuerySet.union


Đây là một giải pháp tốt hơn cho bộ vấn đề của tôi cần có các giá trị duy nhất.
Đốt tinh thể

Không hoạt động cho hình học geodjango.
MarMat

Bạn nhập công đoàn từ đâu? Nó có phải đến từ một trong số X các truy vấn không?
Jack

Vâng, nó là một phương pháp của queryset.
Udi

Tôi nghĩ rằng nó sẽ xóa các bộ lọc tìm kiếm
Pierre Cordier

76

Bạn có thể sử dụng QuerySetChainlớp dưới đây. Khi sử dụng nó với trình phân trang của Django, nó chỉ nên truy cập cơ sở dữ liệu với COUNT(*)các truy vấn cho tất cả các SELECT()truy vấn và truy vấn chỉ cho các truy vấn có bản ghi được hiển thị trên trang hiện tại.

Lưu ý rằng bạn cần chỉ định template_name=nếu sử dụng a QuerySetChainvới các khung nhìn chung, ngay cả khi các truy vấn chuỗi được sử dụng cùng một mô hình.

from itertools import islice, chain

class QuerySetChain(object):
    """
    Chains multiple subquerysets (possibly of different models) and behaves as
    one queryset.  Supports minimal methods needed for use with
    django.core.paginator.
    """

    def __init__(self, *subquerysets):
        self.querysets = subquerysets

    def count(self):
        """
        Performs a .count() for all subquerysets and returns the number of
        records as an integer.
        """
        return sum(qs.count() for qs in self.querysets)

    def _clone(self):
        "Returns a clone of this queryset chain"
        return self.__class__(*self.querysets)

    def _all(self):
        "Iterates records in all subquerysets"
        return chain(*self.querysets)

    def __getitem__(self, ndx):
        """
        Retrieves an item or slice from the chained set of results from all
        subquerysets.
        """
        if type(ndx) is slice:
            return list(islice(self._all(), ndx.start, ndx.stop, ndx.step or 1))
        else:
            return islice(self._all(), ndx, ndx+1).next()

Trong ví dụ của bạn, cách sử dụng sẽ là:

pages = Page.objects.filter(Q(title__icontains=cleaned_search_term) |
                            Q(body__icontains=cleaned_search_term))
articles = Article.objects.filter(Q(title__icontains=cleaned_search_term) |
                                  Q(body__icontains=cleaned_search_term) |
                                  Q(tags__icontains=cleaned_search_term))
posts = Post.objects.filter(Q(title__icontains=cleaned_search_term) |
                            Q(body__icontains=cleaned_search_term) | 
                            Q(tags__icontains=cleaned_search_term))
matches = QuerySetChain(pages, articles, posts)

Sau đó sử dụng matchesvới trình phân trang như bạn đã sử dụng result_listtrong ví dụ của mình.

Các itertoolsmô-đun được giới thiệu vào Python 2.3, vì vậy nó nên có sẵn trong tất cả các phiên bản Python Django chạy trên.


5
Cách tiếp cận hay, nhưng một vấn đề tôi thấy ở đây là các bộ truy vấn được gắn thêm "từ đầu đến cuối". Điều gì xảy ra nếu mỗi bộ truy vấn được sắp xếp theo ngày và một bộ cần kết hợp cũng được sắp xếp theo ngày?
hasen

Điều này chắc chắn trông đầy hứa hẹn, tuyệt vời, tôi sẽ phải thử nó, nhưng tôi không có thời gian hôm nay. Tôi sẽ liên lạc lại với bạn nếu nó giải quyết được vấn đề của tôi. Công việc tuyệt vời
espenhogbakk

Ok, tôi đã phải thử hôm nay, nhưng nó không hoạt động, đầu tiên nó phàn nàn rằng nó không phải thuộc tính _clone nên tôi đã thêm cái đó, chỉ sao chép _all và nó đã hoạt động, nhưng có vẻ như trình phân trang có vấn đề với bộ truy vấn này. Tôi nhận được lỗi phân trang này: "len () của đối tượng chưa hợp nhất"
espenhogbakk

1
@Espen Thư viện Python: pdb, đăng nhập. Bên ngoài: IPython, ipdb, django-log, django-debug-thanh công cụ, django-lệnh-phần mở rộng, werkzeug. Sử dụng các câu lệnh in trong mã hoặc sử dụng mô-đun đăng nhập. Trên hết, học cách hướng nội trong vỏ. Google cho các bài đăng trên blog về gỡ lỗi Django. Rất vui được giúp đỡ!
akaihola

4
@patrick xem djangosnippets.org/snippets/1103djangosnippets.org/snippets/1933 - đặc biệt sau này là một giải pháp rất toàn diện
akaihola

27

Nhược điểm lớn của cách tiếp cận hiện tại của bạn là không hiệu quả với các tập kết quả tìm kiếm lớn, vì bạn phải kéo xuống toàn bộ tập kết quả từ cơ sở dữ liệu mỗi lần, mặc dù bạn chỉ có ý định hiển thị một trang kết quả.

Để chỉ kéo xuống các đối tượng bạn thực sự cần từ cơ sở dữ liệu, bạn phải sử dụng phân trang trên Truy vấn, chứ không phải danh sách. Nếu bạn làm điều này, Django thực sự cắt các Truy vấn trước khi truy vấn được thực thi, do đó, truy vấn SQL sẽ sử dụng OFFSET và LIMIT để chỉ nhận các bản ghi mà bạn sẽ thực sự hiển thị. Nhưng bạn không thể làm điều này trừ khi bạn có thể nhồi nhét tìm kiếm của mình vào một truy vấn nào đó.

Cho rằng cả ba mô hình của bạn đều có tiêu đề và trường cơ thể, tại sao không sử dụng kế thừa mô hình ? Chỉ cần có tất cả ba mô hình kế thừa từ một tổ tiên chung có tiêu đề và cơ thể, và thực hiện tìm kiếm dưới dạng một truy vấn duy nhất trên mô hình tổ tiên.


23

Trong trường hợp bạn muốn xâu chuỗi nhiều truy vấn, hãy thử điều này:

from itertools import chain
result = list(chain(*docs))

Trong đó: docs là danh sách các truy vấn



8

Điều này có thể đạt được bằng hai cách.

Cách 1 để làm điều này

Sử dụng toán tử union cho queryset |để lấy union của hai queryset. Nếu cả hai bộ truy vấn thuộc cùng một mô hình / mô hình đơn lẻ thì có thể kết hợp các truy vấn bằng cách sử dụng toán tử union.

Cho một ví dụ

pagelist1 = Page.objects.filter(
    Q(title__icontains=cleaned_search_term) | 
    Q(body__icontains=cleaned_search_term))
pagelist2 = Page.objects.filter(
    Q(title__icontains=cleaned_search_term) | 
    Q(body__icontains=cleaned_search_term))
combined_list = pagelist1 | pagelist2 # this would take union of two querysets

Cách thứ 2 để làm điều này

Một cách khác để đạt được hoạt động kết hợp giữa hai bộ truy vấn là sử dụng hàm chuỗi itertools .

from itertools import chain
combined_results = list(chain(pagelist1, pagelist2))

7

Yêu cầu: Django==2.0.2 ,django-querysetsequence==0.8

Trong trường hợp bạn muốn kết hợp querysetsvà vẫn đi ra với một QuerySet, bạn có thể muốn kiểm tra trình tự django-queryset-Sequence .

Nhưng một lưu ý về nó. Nó chỉ mất hai querysetsnhư là đối số của nó. Nhưng với python reducebạn luôn có thể áp dụng nó cho nhiều querysets.

from functools import reduce
from queryset_sequence import QuerySetSequence

combined_queryset = reduce(QuerySetSequence, list_of_queryset)

Và đó là nó. Dưới đây là một tình huống tôi gặp phải và cách tôi làm việc list comprehension, reducedjango-queryset-sequence

from functools import reduce
from django.shortcuts import render    
from queryset_sequence import QuerySetSequence

class People(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    mentor = models.ForeignKey('self', null=True, on_delete=models.SET_NULL, related_name='my_mentees')

class Book(models.Model):
    name = models.CharField(max_length=20)
    owner = models.ForeignKey(Student, on_delete=models.CASCADE)

# as a mentor, I want to see all the books owned by all my mentees in one view.
def mentee_books(request):
    template = "my_mentee_books.html"
    mentor = People.objects.get(user=request.user)
    my_mentees = mentor.my_mentees.all() # returns QuerySet of all my mentees
    mentee_books = reduce(QuerySetSequence, [each.book_set.all() for each in my_mentees])

    return render(request, template, {'mentee_books' : mentee_books})

1
Không Book.objects.filter(owner__mentor=mentor)làm điều tương tự? Tôi không chắc đây là trường hợp sử dụng hợp lệ. Tôi nghĩ rằng Bookcó thể cần phải có nhiều owners trước khi bạn cần bắt đầu làm bất cứ điều gì như thế này.
Will S

Vâng, nó làm điều tương tự. Tôi đã thử nó. Dù sao, có lẽ điều này có thể hữu ích trong một số tình huống khác. Cảm ơn đã chỉ ra rằng. Bạn không chính xác bắt đầu biết tất cả các phím tắt khi mới bắt đầu. Đôi khi bạn phải đi trên con đường quanh co tải để đánh giá cao con ruồi
chidimo

6

Đây là một ý tưởng ... chỉ cần kéo xuống một trang đầy đủ kết quả từ mỗi trong số ba và sau đó loại bỏ 20 trang ít hữu ích nhất ... điều này giúp loại bỏ các truy vấn lớn và theo cách đó bạn chỉ hy sinh một hiệu suất nhỏ thay vì rất nhiều


1

Điều này sẽ làm công việc mà không sử dụng bất kỳ libs khác

result_list = list(page_list) + list(article_list) + list(post_list)

-1

Hàm đệ quy này nối các mảng truy vấn thành một bộ truy vấn.

def merge_query(ar):
    if len(ar) ==0:
        return [ar]
    while len(ar)>1:
        tmp=ar[0] | ar[1]
        ar[0]=tmp
        ar.pop(1)
        return ar

1
Tôi thực sự bị mất.
lycuid

chúng tôi kết hợp kết quả truy vấn nó không thể được sử dụng vào thời gian chạy và ý tưởng thực sự tồi tệ để làm điều đó. bởi vì đôi khi nó thêm sự trùng lặp so với kết quả.
Devang Hingu
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.