Bagaimana cara menggabungkan dua queryset atau lebih dalam tampilan Django?

654

Saya mencoba membangun pencarian untuk situs Django yang saya bangun, dan dalam pencarian itu, saya mencari dalam 3 model yang berbeda. Dan untuk mendapatkan pagination pada daftar hasil pencarian, saya ingin menggunakan tampilan object_list generik untuk menampilkan hasilnya. Tetapi untuk melakukan itu, saya harus menggabungkan 3 querysets menjadi satu.

Bagaimana saya bisa melakukan itu? Saya sudah mencoba ini:

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

Tetapi ini tidak berhasil. Saya mendapatkan kesalahan saat mencoba menggunakan daftar itu dalam tampilan umum. Daftar ini tidak memiliki atribut clone.

Adakah yang tahu bagaimana saya bisa menggabungkan ketiga daftar page_list,, article_listdan post_list?

espenhogbakk
sumber
Sepertinya t_rybik telah menciptakan solusi komprehensif di djangosnippets.org/snippets/1933
akaihola
Untuk mencari lebih baik menggunakan solusi khusus seperti Haystack - sangat fleksibel.
minder
1
Pengguna Django 1.11 dan abv, lihat jawaban ini - stackoverflow.com/a/42186970/6003362
Sahil Agarwal
Catatan : pertanyaan terbatas pada kasus yang sangat langka ketika setelah menggabungkan 3 model yang berbeda bersama-sama Anda tidak perlu mengekstraksi model lagi pada daftar untuk membedakan data pada jenis. Untuk sebagian besar kasus - jika perbedaan diharapkan - itu akan antarmuka yang salah. Untuk model yang sama: lihat jawaban tentang union.
Sławomir Lenart

Jawaban:

1058

Menggabungkan daftar pertanyaan menjadi daftar adalah pendekatan yang paling sederhana. Jika basis data akan dipukul untuk semua queryset (misalnya karena hasilnya perlu disortir), ini tidak akan menambah biaya lebih lanjut.

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

Menggunakan itertools.chainlebih cepat daripada mengulang setiap daftar dan menambahkan elemen satu per satu, karena itertoolsdiimplementasikan dalam C. Ini juga mengkonsumsi lebih sedikit memori daripada mengubah setiap queryset ke dalam daftar sebelum digabungkan.

Sekarang mungkin untuk mengurutkan daftar yang dihasilkan misalnya berdasarkan tanggal (seperti yang diminta dalam komentar hasen j untuk jawaban lain). The sorted()Fungsi nyaman menerima generator dan mengembalikan daftar:

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

Jika Anda menggunakan Python 2.4 atau yang lebih baru, Anda bisa menggunakan attrgetterbukan lambda. Saya ingat membaca tentang hal itu menjadi lebih cepat, tetapi saya tidak melihat perbedaan kecepatan yang terlihat untuk satu juta daftar item.

from operator import attrgetter
result_list = sorted(
    chain(page_list, article_list, post_list),
    key=attrgetter('date_created'))
akaihola
sumber
14
Jika menggabungkan kueryset dari tabel yang sama untuk melakukan kueri ATAU, dan memiliki duplikat baris Anda dapat menghilangkannya dengan fungsi groupby: from itertools import groupby unique_results = [rows.next() for (key, rows) in groupby(result_list, key=lambda obj: obj.id)]
Josh Russo
1
Ok, jadi nm tentang fungsi groupby dalam konteks ini. Dengan fungsi Q Anda seharusnya dapat melakukan kueri ATAU apa pun yang Anda butuhkan: https://docs.djangoproject.com/en/1.3/topics/db/queries/#complex-lookups-with-q-objects
Josh Russo
2
@apelliciari Chain menggunakan jauh lebih sedikit memori daripada list.extend, karena itu tidak perlu memuat kedua daftar sepenuhnya ke dalam memori.
Dan Gayle
2
@AWrightIV Inilah versi baru tautan itu: docs.djangoproject.com/en/1.8/topics/db/queries/…
Josh Russo
1
mencoba approacg ini tetapi harus'list' object has no attribute 'complex_filter'
grillazz
466

Coba ini:

matches = pages | articles | posts

Ini mempertahankan semua fungsi querysets yang bagus jika Anda mau order_byatau serupa.

Harap dicatat: ini tidak berfungsi pada querysets dari dua model yang berbeda.

Daniel Holmes
sumber
10
Tidak bekerja pada querysets yang diiris. Atau apakah saya melewatkan sesuatu?
sthzg
1
Saya dulu bergabung dengan queryset menggunakan "|" tetapi tidak selalu berfungsi dengan baik. Lebih baik menggunakan "Q": docs.djangoproject.com/en/dev/topics/db/queries/…
Ignacio Pérez
1
Tampaknya tidak membuat duplikat, menggunakan Django 1.6.
Teekin
15
Berikut |adalah operator serikat pekerja, bukan bitwise ATAU.
e100
6
@ E100 tidak, itu bukan operator serikat pekerja. django membebani bitwise ATAU operator: github.com/django/django/blob/master/django/db/models/…
shangxiao
109

Terkait, untuk mencampur querysets dari model yang sama, atau untuk bidang serupa dari beberapa model, Dimulai dengan Django 1.11 sebuah qs.union()metode juga tersedia:

union()

union(*other_qs, all=False)

Baru di Django 1.11 . Menggunakan operator UNION SQL untuk menggabungkan hasil dari dua atau lebih QuerySets. Sebagai contoh:

>>> qs1.union(qs2, qs3)

Operator UNION hanya memilih nilai yang berbeda secara default. Untuk mengizinkan nilai duplikat, gunakan argumen all = True.

union (), persimpangan (), dan perbedaan () mengembalikan contoh model dari tipe QuerySet pertama bahkan jika argumennya adalah QuerySet dari model lain. Melewati berbagai model berfungsi selama daftar SELECT sama di semua QuerySets (setidaknya jenisnya, namanya tidak masalah asalkan jenisnya dalam urutan yang sama).

Selain itu, hanya LIMIT, OFFSET, dan ORDER BY (yaitu slicing dan order_by ()) yang diizinkan pada QuerySet yang dihasilkan. Lebih lanjut, basis data membatasi operasi apa yang diizinkan dalam kueri gabungan. Misalnya, sebagian besar database tidak mengizinkan LIMIT atau OFFSET dalam kueri gabungan.

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

Udi
sumber
Ini adalah solusi yang lebih baik untuk set masalah saya yang perlu memiliki nilai unik.
Burning Crystals
Tidak berfungsi untuk geometri geodjango.
MarMat
Dari mana Anda mengimpor serikat pekerja? Apakah harus berasal dari salah satu dari jumlah kueryset X?
Jack
Ya, ini adalah metode queryset.
Udi
Saya pikir itu menghapus filter pencarian
Pierre Cordier
76

Anda dapat menggunakan QuerySetChainkelas di bawah ini. Saat menggunakannya dengan paginator Django, seharusnya hanya mengenai database dengan COUNT(*)permintaan untuk semua querysets dan SELECT()queries hanya untuk querysets yang catatannya ditampilkan pada halaman saat ini.

Perhatikan bahwa Anda perlu menentukan template_name=apakah menggunakan QuerySetChaindengan tampilan umum, bahkan jika kueri rantai dirantai semua menggunakan model yang sama.

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

Dalam contoh Anda, penggunaannya adalah:

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)

Kemudian gunakan matchesdengan paginator seperti yang Anda gunakan result_listdalam contoh Anda.

The itertoolsModul diperkenalkan dengan Python 2.3, sehingga harus tersedia di semua versi Python Django berjalan pada.

akaihola
sumber
5
Pendekatan yang bagus, tetapi satu masalah yang saya lihat di sini adalah bahwa set kueri ditambahkan "head-to-tail". Bagaimana jika setiap queryset dipesan berdasarkan tanggal dan satu membutuhkan kombinasi-set juga dipesan berdasarkan tanggal?
hasen
Tentu ini terlihat menjanjikan, bagus, saya harus mencobanya, tetapi saya tidak punya waktu hari ini. Saya akan menghubungi Anda jika ini menyelesaikan masalah saya. Kerja bagus.
espenhogbakk
Ok, saya harus mencoba hari ini, tetapi tidak berhasil, pertama mengeluh bahwa itu tidak perlu atribut _clone jadi saya menambahkan satu, hanya menyalin _all dan yang bekerja, tetapi tampaknya paginator memiliki beberapa masalah dengan queryset ini. Saya mendapatkan kesalahan paginator ini: "len () objek tidak
berukuran
1
@Espen Python library: pdb, logging. Eksternal: IPython, ipdb, django-logging, django-debug-toolbar, django-command-extensions, werkzeug. Gunakan pernyataan cetak dalam kode atau gunakan modul logging. Yang terpenting, belajarlah untuk mengintrospeksi diri. Google untuk posting blog tentang debugging Django. Senang untuk membantu!
akaihola
4
@ Patrick lihat djangosnippets.org/snippets/1103 dan djangosnippets.org/snippets/1933 - terutama yang terakhir adalah solusi yang sangat komprehensif
akaihola
27

Kelemahan besar dari pendekatan Anda saat ini adalah inefisiensi dengan set hasil pencarian yang besar, karena Anda harus menarik seluruh set hasil dari database setiap kali, meskipun Anda hanya bermaksud menampilkan satu halaman hasil.

Untuk hanya menarik objek yang sebenarnya Anda butuhkan dari database, Anda harus menggunakan pagination pada QuerySet, bukan daftar. Jika Anda melakukan ini, Django sebenarnya mengiris QuerySet sebelum kueri dieksekusi, jadi kueri SQL akan menggunakan OFFSET dan LIMIT untuk hanya mendapatkan catatan yang benar-benar akan Anda tampilkan. Tetapi Anda tidak dapat melakukan ini kecuali jika Anda dapat menjejalkan pencarian Anda ke dalam satu permintaan.

Mengingat ketiga model Anda memiliki bidang judul dan badan, mengapa tidak menggunakan warisan model ? Hanya memiliki ketiga model yang diwarisi dari leluhur yang sama yang memiliki judul dan tubuh, dan melakukan pencarian sebagai permintaan tunggal pada model leluhur.

Carl Meyer
sumber
23

Jika Anda ingin mengaitkan banyak kueri, coba ini:

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

dimana: docs adalah daftar querysets

vutran
sumber
8

Ini dapat dicapai dengan dua cara baik.

Cara pertama untuk melakukan ini

Gunakan operator gabungan untuk queryset |untuk mengambil gabungan dua queryset. Jika kedua queryset milik model yang sama / model tunggal daripada itu mungkin untuk menggabungkan queryset dengan menggunakan operator serikat.

Sebagai contoh

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

Cara ke-2 untuk melakukan ini

Salah satu cara lain untuk mencapai operasi gabungan antara dua queryset adalah dengan menggunakan fungsi rantai itertools .

from itertools import chain
combined_results = list(chain(pagelist1, pagelist2))
Devang Padhiyar
sumber
7

Persyaratan: Django==2.0.2 ,django-querysetsequence==0.8

Jika Anda ingin menggabungkan querysetsdan masih keluar dengan QuerySet, Anda mungkin ingin memeriksa django-queryset-sequence .

Tapi satu catatan tentang itu. Hanya butuh dua querysetskarena itu argumen. Tetapi dengan python reduceAnda selalu dapat menerapkannya ke beberapa querysets.

from functools import reduce
from queryset_sequence import QuerySetSequence

combined_queryset = reduce(QuerySetSequence, list_of_queryset)

Dan itu saja. Di bawah ini adalah situasi yang saya hadapi dan bagaimana saya bekerja list comprehension, reducedandjango-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})
chidimo
sumber
1
Tidak Book.objects.filter(owner__mentor=mentor)melakukan hal yang sama? Saya tidak yakin ini kasus penggunaan yang valid. Saya pikir Bookmungkin perlu memiliki beberapa ownersebelum Anda perlu mulai melakukan hal seperti ini.
Will S
Ya itu melakukan hal yang sama. Saya mencobanya. Bagaimanapun, mungkin ini bisa berguna dalam situasi lain. Terima kasih telah menunjukkannya. Anda tidak benar-benar mulai mengetahui semua pintasan sebagai pemula. Terkadang Anda harus menempuh jalan berliku untuk menghargai lalat gagak
chidimo
6

inilah sebuah ide ... cukup tarik ke bawah satu halaman penuh hasil dari masing-masing dari ketiganya dan kemudian buang 20 yang paling tidak berguna ... ini menghilangkan kueryset besar dan dengan begitu Anda hanya mengorbankan sedikit kinerja alih-alih banyak.

Jiaaro
sumber
1

Ini akan melakukan pekerjaan tanpa menggunakan lib lainnya

result_list = list(page_list) + list(article_list) + list(post_list)
Satyam Faujdar
sumber
-1

Fungsi rekursif ini menggabungkan array queryset menjadi satu queryset.

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
Petr Dvořáček
sumber
1
Saya benar-benar tersesat.
lycuid
kami menggabungkan hasil kueri yang tidak dapat digunakan saat run-time dan itu ide yang sangat buruk untuk melakukannya. karena kadang-kadang itu menambah duplikasi hasil.
Devang Hingu