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


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/1103djangosnippets.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, 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
않습니다 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.