web-dev-qa-db-ja.com

Djangoのビューで2つ以上のクエリセットを組み合わせるにはどうすればいいですか?

私は私が構築しているDjangoサイトの検索を構築しようとしています、そして検索で私は3つの異なるモデルで検索しています。そして検索結果リストのページ付けをするために、一般的なobject_listビューを使って結果を表示したいと思います。しかしそれをするために私は1つに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_listarticle_listおよびpost_listの3つのリストをどのようにマージできるかを誰もが知っていますか?

594
espenhogbakk

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

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

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

結果リストをソートすることが可能になりました。日付順(他の回答に対する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'))
978
akaihola

これを試して:

matches = pages | articles | posts

あなたがorder_byまたは同様にしたいのであればいいクエリセットのすべての機能を保持します。

おっと、これは2つの異なるモデルのクエリセットでは機能しないことに注意してください。

414
bryan

同じモデルのクエリセットを混在させる場合や、いくつかのモデルの似たようなフィールドの場合は、Django 1.11a qs.union() method も利用できます。

union()

union(*other_qs, all=False)

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

>>> qs1.union(qs2, qs3)

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

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

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

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

91
Udi

以下のQuerySetChainクラスを使うことができます。 Djangoのpaginatorと一緒に使うとき、すべてのクエリセットに対してCOUNT(*)クエリを使い、現在のページにレコードが表示されているクエリセットに対してのみSELECT()クエリを使ってデータベースをヒットさせるべきです。

チェーンクエリセットがすべて同じモデルを使用している場合でも、汎用ビューでQuerySetChainを使用する場合はtemplate_name=を指定する必要があることに注意してください。

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)

それからあなたの例でresult_listを使ったのと同じようにmatchesをページ付け子と共に使います。

itertoolsモジュールはPython 2.3で導入されたので、Djangoが動作するすべてのPythonバージョンで利用可能であるべきです。

73
akaihola

1ページの結果しか表示しない場合でも、毎回データベースから結果セット全体を取り出す必要があるため、現在のアプローチの大きな欠点は、大きな検索結果セットでは効率が悪いことです。

データベースから実際に必要なオブジェクトのみを取得するには、リストではなくQuerySetでページ付けを使用する必要があります。これを行うと、Djangoは実際にはクエリが実行される前にQuerySetをスライスします。そのため、SQLクエリはOFFSETとLIMITを使って実際に表示されるレコードのみを取得します。しかし、検索を何らかの形で1つのクエリにまとめることができない限り、これはできません。

3つのモデルすべてにtitleフィールドとbodyフィールドがあることを考えて、なぜ モデル継承 を使用しないのですか? 3つのモデルすべてにtitleとbodyを持つ共通の先祖から継承させ、先祖モデルに対する単一のクエリとして検索を実行するだけです。

26
Carl Meyer

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

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

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

20
vutran
DATE_FIELD_MAPPING = {
    Model1: 'date',
    Model2: 'pubdate',
}

def my_key_func(obj):
    return getattr(obj, DATE_FIELD_MAPPING[type(obj)])

And then sorted(chain(Model1.objects.all(), Model2.objects.all()), key=my_key_func)

https://groups.google.com/forum/#!topic/Django-users/6wUNuJa4jVw から引用しました。参照Alex Gaynor

15
ray6080

これはアイデアです。3つのそれぞれから1ページ分の結果をすべてプルダウンしてから、最も有用性の低い20の結果を捨てるだけです。

5
Jiaaro

必要条件:Django==2.0.2Django-querysetsequence==0.8

querysetsを結合し、それでもQuerySetを出したい場合は、 Django-queryset-sequence をチェックアウトするとよいでしょう。

しかし、それについて一つ注意してください。引数として2つのquerysetsのみを取ります。しかし、pythonのreduceでは、いつでもそれを複数のquerysetに適用できます。

from functools import reduce
from queryset_sequence import QuerySetSequence

combined_queryset = reduce(QuerySetSequence, list_of_queryset)

以上です。以下は私が遭遇した状況と私がlist comprehensionreduceDjango-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})
5
chidimo

これは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))
4
Devang Padhiyar

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