私は私が構築している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_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
を使用できます。私はそれがより速いことについて読んだことを覚えています、しかし私は百万のアイテムリストのために顕著な速度の違いを見ませんでした。
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.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
以下の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バージョンで利用可能であるべきです。
1ページの結果しか表示しない場合でも、毎回データベースから結果セット全体を取り出す必要があるため、現在のアプローチの大きな欠点は、大きな検索結果セットでは効率が悪いことです。
データベースから実際に必要なオブジェクトのみを取得するには、リストではなくQuerySetでページ付けを使用する必要があります。これを行うと、Djangoは実際にはクエリが実行される前にQuerySetをスライスします。そのため、SQLクエリはOFFSETとLIMITを使って実際に表示されるレコードのみを取得します。しかし、検索を何らかの形で1つのクエリにまとめることができない限り、これはできません。
3つのモデルすべてにtitleフィールドとbodyフィールドがあることを考えて、なぜ モデル継承 を使用しないのですか? 3つのモデルすべてにtitleとbodyを持つ共通の先祖から継承させ、先祖モデルに対する単一のクエリとして検索を実行するだけです。
たくさんのクエリセットをチェインしたい場合は、これを試してください。
from itertools import chain
result = list(chain(*docs))
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 をチェックアウトするとよいでしょう。
しかし、それについて一つ注意してください。引数として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