장고 뷰에서 둘 이상의 쿼리 세트를 결합하는 방법은 무엇입니까?


654

내가 구축하고있는 Django 사이트에 대한 검색을 구축하려고하고 있는데,이 검색에서 3 가지 모델을 검색하고 있습니다. 검색 결과 목록에서 페이지 매김을 얻으려면 일반 object_list보기를 사용하여 결과를 표시하고 싶습니다. 그러나 그렇게하려면 3 개의 쿼리 세트를 하나로 병합해야합니다.

어떻게해야합니까? 나는 이것을 시도했다 :

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")

그러나 이것은 작동하지 않습니다. 일반보기에서 해당 목록을 사용하려고하면 오류가 발생합니다. 목록에 복제 속성이 없습니다.

사람의 알고 나는 세 가지 목록을 병합 할 수 있는가, 어떻게 page_list, article_list그리고 post_list?


t_rybik이 djangosnippets.org/snippets/1933에서 포괄적 인 솔루션을 만든 것 같습니다
— akaihola

검색에는 Haystack 과 같은 전용 솔루션을 사용하는 것이 좋습니다 . 매우 유연합니다.
— minder

1
- 장고 사용자 1.11 및 ABV는이 대답을 참조 stackoverflow.com/a/42186970/6003362
— 사힐 아가 왈

참고 : 3 가지 모델을 병합 한 후 유형에서 데이터를 구별하기 위해 목록에서 모델을 다시 추출 할 필요가없는 경우는 매우 드문 경우로 제한됩니다. 대부분의 경우-구별이 예상되는 경우 잘못된 인터페이스입니다. 동일한 모델 :에 대한 답변을 참조하십시오 union.
— Sławomir Lenart

답변:


1058

쿼리 세트를 목록으로 연결하는 것이 가장 간단한 방법입니다. 어쨌든 결과가 정렬되어야하기 때문에 모든 쿼리 세트에 대해 데이터베이스에 도달하면 추가 비용이 발생하지 않습니다.

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

사용하기 itertools.chain때문에, 각 속도에서 반복 및 요소 하나씩 부가보다 itertools또한 연접 전에 각각의 검색어 목록으로 전환보다 적은 메모리 소모 C. 구현된다.

이제 결과 목록을 날짜별로 정렬 할 수 있습니다 (다른 답변에 대한 hasen j의 의견에서 요청한대로). 이 sorted()함수는 생성기를 편리하게 받아들이고 목록을 반환합니다.

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

Python 2.4 이상을 사용 attrgetter하는 경우 람다 대신 사용할 수 있습니다 . 나는 그것이 더 빠르다는 것에 대해 읽은 것을 기억하지만, 백만 개의 항목 목록에서 눈에 띄는 속도 차이는 보지 못했습니다.

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

14
동일한 테이블에서 쿼리 세트를 병합하여 OR 쿼리를 수행하고 행이 복제 된 경우 groupby 함수를 사용하여이를 제거 할 수 있습니다. from itertools import groupby unique_results = [rows.next() for (key, rows) in groupby(result_list, key=lambda obj: obj.id)]
— Josh Russo

1
자,이 문맥에서 groupby 함수에 대해서는 nm입니다. Q 기능을 사용하면 필요한 OR 쿼리를 수행 할 수 있어야합니다. https://docs.djangoproject.com/en/1.3/topics/db/queries/#complex-lookups-with-q-objects
— Josh Russo

2
@apelliciari Chain은 list.extend보다 메모리를 상당히 적게 사용합니다. 두 목록을 모두 메모리에 완전히로드 할 필요가 없기 때문입니다.
— Dan Gayle

2
@AWrightIV 해당 링크의 새 버전은 다음과 같습니다. docs.djangoproject.com/en/1.8/topics/db/queries/…
— Josh Russo

1
이 접근 방식을 시도했지만'list' object has no attribute 'complex_filter'
— grillazz

466

이 시도:

matches = pages | articles | posts

그것은 당신이 원 order_by하거나 유사한 경우 좋은 쿼리 세트의 모든 기능을 유지합니다 .

참고 : 두 가지 모델의 쿼리 세트에서는 작동하지 않습니다.


10
그러나 슬라이스 된 쿼리 세트에서는 작동하지 않습니다. 아니면 뭔가 빠졌습니까?
— sthzg

1
"|"를 사용하여 쿼리 세트를 조인했습니다. 그러나 항상 잘 작동하는 것은 아닙니다. 그것은 "Q"를 사용하는 것이 좋습니다 : docs.djangoproject.com/en/dev/topics/db/queries/...
— 이그나시오 페레즈

1
Django 1.6을 사용하여 복제본을 생성하지 않는 것 같습니다.
— Teekin

15
다음 |은 비트 단위 OR이 아닌 집합 공용 연산자입니다.
— e100

6
@ e100 아니오, 집합 조합 연산자가 아닙니다. django는 비트 OR 연산자를 오버로드합니다 : github.com/django/django/blob/master/django/db/models/…
— shangxiao

109

Django 1.11 부터는 동일한 모델의 쿼리 집합을 혼합하거나 몇 가지 모델의 유사한 필드를 혼합 하는 데 사용할 수 있는 qs.union()방법 도 있습니다.

union()

union(*other_qs, all=False)

장고 1.11의 새로운 기능 . SQL의 UNION 연산자를 사용하여 둘 이상의 QuerySet 결과를 결합합니다. 예를 들면 다음과 같습니다.

>>> qs1.union(qs2, qs3)

UNION 연산자는 기본적으로 고유 한 값만 선택합니다. 중복 값을 허용하려면 all = True 인수를 사용하십시오.

union (), interaction () 및 difference ()는 인수가 다른 모델의 QuerySet 인 경우에도 첫 번째 QuerySet 유형의 모델 인스턴스를 반환합니다. SELECT 목록이 모든 QuerySet에서 동일하면 다른 모델을 전달할 수 있습니다 (적어도 유형은 동일한 순서의 유형 인 경우 이름이 중요하지 않음).

또한 LIMIT, OFFSET 및 ORDER BY (예 : 슬라이싱 및 order_by ()) 만 결과 QuerySet에 허용됩니다. 또한 데이터베이스는 결합 된 쿼리에서 허용되는 작업에 제한을 둡니다. 예를 들어, 대부분의 데이터베이스는 결합 된 쿼리에서 LIMIT 또는 OFFSET을 허용하지 않습니다.

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


이것은 고유 한 값을 가져야하는 문제 세트에 대한 더 나은 솔루션입니다.
— 불타는 결정

지장 고 도형에는 작동하지 않습니다.
— MarMat

그래도 어디서 노동 조합을 수입합니까? X 개의 쿼리 세트 중 하나에서 가져와야합니까?
— Jack

예, queryset의 방법입니다.
— Udi

검색 필터가 제거 된 것 같습니다
— Pierre Cordier

76

QuerySetChain아래 클래스 를 사용할 수 있습니다 . Django의 paginator와 함께 사용하면 COUNT(*)모든 쿼리 세트에 대한 SELECT()쿼리 와 현재 페이지에 레코드가 표시되는 쿼리 세트에 대한 쿼리만으로 데이터베이스에 도달해야 합니다.

체인 쿼리 세트가 모두 동일한 모델을 사용하더라도 일반 뷰와 함께를 template_name=사용할 경우 지정해야합니다 QuerySetChain.

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()

귀하의 예에서 사용법은 다음과 같습니다.

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)

그런 다음 예제에서 matches사용한 것처럼 페이지 매김 장치와 함께 사용 result_list하십시오.

이 itertools모듈은 Python 2.3에서 도입되었으므로 Django가 실행되는 모든 Python 버전에서 사용할 수 있어야합니다.


5
좋은 접근 방법이지만 여기서 볼 수있는 한 가지 문제는 쿼리 세트에 "head-to-tail"이 추가된다는 것입니다. 각 쿼리 세트가 날짜별로 정렬되어 있고 결합 된 세트도 날짜별로 정렬해야하는 경우 어떻게해야합니까?
— hasen jan

이것은 확실히 유망한 것처럼 보입니다. 좋습니다. 나는 그것을 시도해야하지만 오늘은 시간이 없습니다. 문제가 해결되면 다시 연락 드리겠습니다. 훌륭한 일.
— espenhogbakk

좋아, 오늘 시도했지만 작동하지 않았다. 먼저 속성을 _clone 할 필요가 없다고 불평했기 때문에 그 속성을 추가하고 _all을 복사하고 작동했지만 paginator 에이 쿼리 세트에 문제가있는 것 같습니다. "크기를 지정하지 않은 객체의) (렌":이 매기기 오류가
— espenhogbakk

1
@Espen Python 라이브러리 : pdb, 로깅. 외부 : IPython, ipdb, django-logging, django-debug-toolbar, django-command-extensions, werkzeug. 코드에서 인쇄 문을 사용하거나 로깅 모듈을 사용하십시오. 무엇보다도, 껍질에서 내성을 배우는 법을 배웁니다. Django 디버깅에 대한 블로그 게시물을위한 Google 기쁘다!
— akaihola

4
@patrick 참조 djangosnippets.org/snippets/1103 및 djangosnippets.org/snippets/1933 - epecially 후자는 매우 포괄적 인 솔루션입니다
— akaihola

27

현재 접근 방식의 큰 단점은 한 페이지의 결과 만 표시하려는 경우에도 매번 데이터베이스에서 전체 결과 세트를 풀다운해야하기 때문에 검색 결과 세트가 크면 비효율적입니다.

데이터베이스에서 실제로 필요한 객체 만 풀다운하려면 목록이 아닌 QuerySet에서 페이지 매김을 사용해야합니다. 이렇게하면 Django는 실제로 쿼리가 실행되기 전에 QuerySet을 슬라이스하므로 SQL 쿼리는 OFFSET 및 LIMIT를 사용하여 실제로 표시 할 레코드 만 가져옵니다. 그러나 어떻게 든 검색을 단일 쿼리로 구성 할 수 없다면이 작업을 수행 할 수 없습니다.

세 모델 모두에 제목 및 본문 필드가 있으므로 모델 상속을 사용하지 않겠습니까? 세 모델 모두 제목과 본문이있는 공통 조상에서 상속하고 조상 모델에 대한 단일 쿼리로 검색을 수행하십시오.


23

많은 쿼리 세트를 연결하려면 다음을 시도하십시오.

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

여기서 : docs는 쿼리 세트 목록입니다.



8

이것은 두 가지 방법으로 달성 할 수 있습니다.

이 작업을 수행하는 첫 번째 방법

queryset |에 대해 union 연산자를 사용 하여 두 개의 queryset을 결합하십시오. 두 쿼리 세트가 동일한 모델 / 단일 모델에 속하는 경우 공용체 연산자를 사용하여 쿼리 세트를 결합 할 수 있습니다.

인스턴스

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

이 작업을 수행하는 두 번째 방법

두 queryset 사이의 결합 작업을 수행하는 또 다른 방법은 itertools chain 함수 를 사용하는 것입니다.

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

7

요구 사항 : Django==2.0.2 ,django-querysetsequence==0.8

결합 querysets하고 여전히을 QuerySet원한다면 django-queryset-sequence 를 확인하십시오 .

그러나 그것에 대한 하나의 메모. querysets논쟁의 여지 가 두 가지 밖에 없습니다 . 그러나 파이썬 reduce을 사용하면 언제든지 여러 개에 적용 할 수 있습니다 queryset.

from functools import reduce
from queryset_sequence import QuerySetSequence

combined_queryset = reduce(QuerySetSequence, list_of_queryset)

그리고 그게 다야. 아래는 제가 다 퉜다 상황이 내가 사용하는 방법 list comprehension, reduce및django-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
않습니다 Book.objects.filter(owner__mentor=mentor)같은 일을하지? 이것이 유효한 유스 케이스인지 확실하지 않습니다. 나는 당신이 이와 같은 일을 시작하기 전에 Book여러 가지를 가질 필요가 있다고 생각합니다 owner.
— Will S

예, 똑같은 일을합니다. 나는 그것을 시도했다. 어쨌든, 이것은 아마도 다른 상황에서 유용 할 수 있습니다. 지적 해 주셔서 감사합니다. 초보자로서 모든 지름길을 정확히 아는 것은 아닙니다. 때때로 당신은 까마귀의 비행을 감상하기 위해 짐 굴곡 도로를 여행해야합니다
— chidimo

6

여기 하나의 아이디어가 있습니다. 세 개 각각에서 하나의 전체 결과 페이지를 가져온 다음 가장 유용한 20 개를 버립니다. 이렇게하면 큰 쿼리 집합이 제거되므로 많은 대신에 약간의 성능 만 희생 할 수 있습니다.


1

이것은 다른 라이브러리를 사용하지 않고 작업을 수행합니다.

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

-1

이 재귀 함수는 쿼리 집합 배열을 하나의 쿼리 집합으로 연결합니다.

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
나는 말 그대로 길을 잃었다.
— lycuid

우리는 쿼리 결과를 결합하여 런타임에 사용할 수 없으며 그렇게하는 것이 나쁜 생각입니다. 언젠가 결과에 중복을 추가하기 때문입니다.
— Devang Hingu
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.