Djangoビューで2つ以上のクエリセットを組み合わせる方法は?


654

私は構築しているDjangoサイトの検索を構築しようとしています。その検索では、3つの異なるモデルで検索しています。そして、検索結果リストのページネーションを取得するために、汎用のobject_listビューを使用して結果を表示したいと思います。しかし、そのためには、3つのクエリセットを1つにマージする必要があります。

どうやってやるの?私はこれを試しました:

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

しかし、これは機能しません。汎用ビューでそのリストを使用しようとすると、エラーが発生します。リストにはクローン属性がありません。

誰が、私は3つのリストをマージする方法を知っていますpage_listarticle_listpost_list


t_rybikのように見えるがで包括的なソリューションを作成しましたdjangosnippets.org/snippets/1933
akaihola

検索には、Haystackのような専用のソリューションを使用する方が適切です。非常に柔軟です。
2010

1
Djangoユーザー1.11およびabv、この回答を参照-stackoverflow.com/a/42186970/6003362
Sahil Agarwal

:質問は、3つの異なるモデルをマージした後、タイプのデータを区別するためにリストでモデルを再度抽出する必要がないという非常にまれなケースに限定されます。ほとんどの場合-区別が予想される場合-インターフェースが間違っています。同じモデルの場合:に関する回答を参照してくださいunion
SławomirLenart

回答:


1058

クエリセットをリストに連結するのが最も簡単な方法です。いずれにしても、データベースがすべてのクエリセットにヒットする場合(たとえば、結果をソートする必要があるため)、これによってコストが増えることはありません。

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

を使用するitertools.chainitertools、Cで実装されているため、各リストをループして要素を1つずつ追加するよりも高速です。また、連結する前に各クエリセットをリストに変換するよりもメモリの消費量が少なくなります。

これで、結果リストを日付などで並べ替えることができます(別の回答に対するhasen jのコメントで要求されているとおり)。このsorted()関数はジェネレータを受け取り、リストを返します。

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

Python 2.4以降を使用している場合attrgetterは、ラムダの代わりに使用できます。私はそれがより速いことについて読んだことを覚えていますが、私は100万のアイテムリストの顕著な速度の違いを見ませんでした。

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

14
同じテーブルからクエリセットをマージするまたはクエリを実行し、重複行を持っている場合は、GROUPBY機能とそれらを排除することができます: from itertools import groupby unique_results = [rows.next() for (key, rows) in groupby(result_list, key=lambda obj: obj.id)]
ジョシュ・ルッソ

1
OK、このコンテキストでのgroupby関数についてnmです。Q関数を使用すると、必要なORクエリを実行できます。https
Josh Russo

2
@apelliciariチェーンは、両方のリストをメモリに完全にロードする必要がないため、list.extendよりも大幅に少ないメモリを使用します。
Dan Gayle、2015

2
@AWrightIVここではそのリンクの新しいバージョンがあります:docs.djangoproject.com/en/1.8/topics/db/queries/...
ジョシュ・ルッソ

1
このアプローチを試してみてください'list' object has no attribute 'complex_filter'
グリルズ

466

これを試して:

matches = pages | articles | posts

クエリセットのすべての機能が保持されているので、必要に応じて、order_byまたは類似した機能を利用できます。

注:これは、2つの異なるモデルのクエリセットでは機能しません。


10
ただし、スライスされたクエリセットでは機能しません。それとも何か不足していますか?
sthzg 2014

1
以前は「|」を使用してクエリセットを結合していました しかし、常にうまくいくとは限りません。これは、「Q」を使用することをお勧めします:docs.djangoproject.com/en/dev/topics/db/queries/...
イグナシオ・ペレス

1
Django 1.6を使用して、重複を作成していないようです。
Teekin 2014年

15
これ|は、ビットごとのORではなく、集合和演算子です。
e100 2015年

6
@ e100いいえ、それは和集合演算子ではありません。ジャンゴ過負荷ビット単位のOR演算子:github.com/django/django/blob/master/django/db/models/...
shangxiao

109

関連して、同じモデルからのクエリセットを混合するため、またはいくつかのモデルからの類似のフィールドのために、Django 1.11以降qs.union()メソッドも利用できます。

union()

union(*other_qs, all=False)

Django 1.11の新機能。SQLのUNION演算子を使用して、2つ以上のクエリセットの結果を結合します。例えば:

>>> qs1.union(qs2, qs3)

UNION演算子は、デフォルトで個別の値のみを選択します。値の重複を許可するには、all = True引数を使用します。

union()、intersection()、difference()は、引数が他のモデルのクエリセットであっても、最初のクエリセットのタイプのモデルインスタンスを返します。SELECTリストがすべてのQuerySetで同じである限り、異なるモデルを渡すことができます(少なくとも型、名前は型が同じ順序である限り問題ではありません)。

また、結果のQuerySetでは、LIMIT、OFFSET、およびORDER BY(つまり、スライスとorder_by())のみが許可されます。さらに、データベースは、結合されたクエリで許可される操作に制限を課します。たとえば、ほとんどのデータベースでは、結合されたクエリでLIMITまたはOFFSETを使用できません。

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


これは、一意の値を持つ必要がある私の問題セットのより良い解決策です。
Burning Crystals 2017

geodjangoジオメトリでは機能しません。
MarMat

どこからユニオンをインポートしますか?X個のクエリセットの1つから取得する必要がありますか?
ジャック

はい、それはquerysetのメソッドです。
ウディ

検索フィルターは削除されると思います
ピエールコルディエ

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
良いアプローチですが、ここで私が見る1つの問題は、クエリセットに「head-to-tail」が付加されることです。各クエリセットが日付順に並べ替えられ、組み合わせセットも日付順に並べ替える必要がある場合はどうなりますか?
Hasen

これは確かに有望ですばらしいように見えます。私はそれを試さなければなりませんが、今日は時間がありません。それで問題が解決した場合は、折り返しご連絡いたします。すごい仕事。
espenhogbakk 2009年

OK、今日は試さなければなりませんでしたが、機能しませんでした。最初に_clone属性は必要ないという不満があったので、属性を追加し、_allをコピーしただけで機能しましたが、このクエリセットではページ編集者に問題があるようです。私はこのページネーターエラーを受け取ります:「サイズのないオブジェクトのlen()」
espenhogbakk 2009年

1
@Espen Pythonライブラリ:pdb、ロギング。外部:IPython、ipdb、django-logging、django-debug-toolbar、django-command-extensions、werkzeug。コードで印刷ステートメントを使用するか、ロギングモジュールを使用します。とりわけ、シェルで内省することを学びます。Djangoのデバッグに関するブログ投稿のGoogle。助けてくれてうれしい!
akaihola 2009年

4
参照@patrick djangosnippets.org/snippets/1103djangosnippets.org/snippets/1933 epecially後者は非常に包括的なソリューションである-
akaihola

27

現在のアプローチの大きな欠点は、結果を1ページだけ表示するつもりでも、毎回データベースから結果セット全体をプルダウンする必要があるため、検索結果セットが大きくて非効率になることです。

データベースから実際に必要なオブジェクトのみをプルダウンするには、リストではなく、クエリセットでページ分割を使用する必要があります。これを行うと、Djangoはクエリが実行される前に実際にQuerySetをスライスするため、SQLクエリはOFFSETとLIMITを使用して、実際に表示するレコードのみを取得します。しかし、何らかの方法で検索を単一のクエリに詰め込めない限り、これを行うことはできません。

3つのモデルすべてにタイトルフィールドと本文フィールドがある場合、モデル継承を使用しないのはなぜですか?3つのモデルすべてに、タイトルと本文を持つ共通の祖先を継承させ、祖先モデルに対して単一のクエリとして検索を実行します。


23

多くのクエリセットをチェーンしたい場合は、これを試してください:

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

ここで、docsはクエリセットのリストです。



8

これは、2つの方法で実現できます。

これを行う最初の方法

クエリセットのユニオン演算子を使用して|、2つのクエリセットのユニオンを取得します。両方のクエリセットが同じモデルまたは単一のモデルに属している場合は、ユニオン演算子を使用してクエリセットを組み合わせることができます。

インスタンスの場合

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

これを行う2番目の方法

2つのクエリセット間の結合操作を実現するもう1つの方法は、itertoolsチェーン関数を使用することです。

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

7

要件: Django==2.0.2django-querysetsequence==0.8

を組み合わせquerysetsてまだで出てくるQuerySet場合は、django-queryset-sequenceをチェックすることをお勧めします

しかし、それについての1つの注意。querysets引数は2つだけです。しかし、Python reduceでは、常に複数querysetのに適用できます。

from functools import reduce
from queryset_sequence import QuerySetSequence

combined_queryset = reduce(QuerySetSequence, list_of_queryset)

以上です。以下は、私はに走ったと私はどのように使用される状況がありlist comprehensionreduceかつ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のが必要になる可能性があります。
ウィルS

ええ、それは同じことをします。私はそれを試してみました。とにかく、おそらくこれは他の状況で役立つかもしれません。ご指摘いただきありがとうございます。あなたは正確に初心者としてすべてのショートカットを知っていることから始めません。時折、カラスのフライを鑑賞するために負荷のかかる道路を旅しなければなりません
chidimo

6

ここにアイデアがあります... 3つそれぞれの結果から1ページ分の結果を引き出し、次に20の最も役に立たない結果を捨てます...これは大きなクエリセットを排除し、その結果、多くではなく小さなパフォーマンスのみを犠牲にします



-1

この再帰関数は、クエリセットの配列を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

クエリ結果を組み合わせると、実行時に使用できなくなります。これを行うのは非常に悪い考えです。結果に重複が追加されることがあるからです。
デバンヒング
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.