Djangoビューで2つ以上のクエリセットを組み合わせる方法は?
-
08-07-2019 - |
質問
私が構築している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")
しかし、これは機能しません。汎用ビューでそのリストを使用しようとするとエラーが発生します。リストにはクローン属性がありません。
page_list
、 article_list
、および post_list
の3つのリストをマージする方法は誰でも知っていますか?
解決
クエリセットをリストに連結するのが最も簡単なアプローチです。とにかくデータベースがすべてのクエリセットでヒットする場合(結果を並べ替える必要があるためなど)、これによりコストは追加されません。
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
を使用できます。私はそれがより高速であることを読んだことを覚えていますが、100万のアイテムリストで顕著な速度の違いは見られませんでした。
from operator import attrgetter
result_list = sorted(
chain(page_list, article_list, post_list),
key=attrgetter('date_created'))
他のヒント
これを試してください:
matches = pages | articles | posts
クエリセットのすべての機能を保持しているので、 order_by
などにしたい場合に便利です。
注意:これは、2つの異なるモデルのクエリセットでは機能しません。
関連、同じモデルのクエリセットを混合するため、またはいくつかのモデルの類似フィールドの場合、 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()は、 引数が次のQuerySetである場合でも、最初のQuerySetのタイプ 他のモデル。異なるモデルを渡すことは、SELECT リストはすべてのQuerySetsで同じです(少なくともタイプ、名前はありません’ t 型が同じ順序である限り重要です。
さらに、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
以下の QuerySetChain
クラスを使用できます。 Djangoのページネータで使用する場合、すべてのクエリセットの 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バージョンで使用可能になります。
現在のアプローチの大きな欠点は、検索結果セットが1ページしか表示されない場合でも、毎回データベースから結果セット全体をプルダウンする必要があるため、検索結果セットが大きいことの非効率性です。
データベースから実際に必要なオブジェクトのみをプルダウンするには、リストではなくQuerySetでページネーションを使用する必要があります。これを行うと、Djangoはクエリが実行される前にQuerySetを実際にスライスするので、SQLクエリはOFFSETとLIMITを使用して、実際に表示するレコードのみを取得します。ただし、何らかの方法で検索を1つのクエリに詰め込めない限り、これを行うことはできません。
3つすべてのモデルにタイトルフィールドと本文フィールドがあることを考えると、モデルの継承? 3つのモデルすべてに、タイトルと本文を持つ共通の祖先を継承させ、祖先モデルで単一のクエリとして検索を実行するだけです。
多くのクエリセットを連鎖させたい場合は、これを試してください:
from itertools import chain
result = list(chain(*docs))
where:docsはクエリセットのリストです
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 を参照してください
ここにアイデアがあります... 3つのそれぞれから結果の完全なページを1つプルダウンし、20の最も有用でないものを捨てます...これにより、大きなクエリセットがなくなり、その代わりに、パフォーマンスを少し犠牲にするだけです。たくさん
要件:
Django == 2.0.2
、 django-querysetsequence == 0.8
querysets
を組み合わせて、引き続き QuerySet
を使用したい場合は、 django-queryset-sequence 。
しかし、それについての1つのメモ。引数として2つの querysets
のみを取ります。ただし、Python 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})
これは、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))
この再帰関数は、クエリセットの配列を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