Django görünümünde iki veya daha fazla sorgu kümesini nasıl birleştiririm?


654

Yaptığım bir Django sitesi için arama yapmaya çalışıyorum ve bu aramada 3 farklı modelde arıyorum. Ve arama sonucu listesinde sayfalandırma almak için, sonuçları görüntülemek için genel bir object_list görünümü kullanmak istiyorum. Ama bunu yapmak için, 3 sorgu kümesini bir araya getirmeliyim.

Bunu nasıl yapabilirim? Bunu denedim:

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

Ama bu işe yaramıyor. Bu listeyi genel görünümde kullanmaya çalıştığımda bir hata alıyorum. Listede klon özelliği eksik.

Üç listeyi nasıl birleştirebileceğimi bilen var mı page_list, article_listve post_list?


T_rybik gibi görünüyor kapsamlı bir çözüm yarattı djangosnippets.org/snippets/1933
akaihola

Arama yapmak için Haystack gibi özel çözümler kullanmak daha iyidir - çok esnektir.
minder

1
Django kullanıcıları 1.11 ve abv, bu cevaba bakın - stackoverflow.com/a/42186970/6003362
Sahil Agarwal

Not : 3 farklı modeli bir araya getirdikten sonra, türlerle ilgili verileri ayırt etmek için listeden modellerin tekrar çıkarılmasına gerek olmadığında soru çok nadir bir durumla sınırlıdır. Çoğu durumda - ayrım beklenirse - yanlış arayüz olacaktır. Aynı model için: hakkındaki cevaplara bakın union.
Sławomir Lenart

Yanıtlar:


1058

Querysets'i bir liste halinde birleştirmek en basit yaklaşımdır. Veritabanı yine de tüm sorgu kümeleri için vurulacaksa (örneğin sonucun sıralanması gerektiğinden), bu ek maliyet getirmez.

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

Kullanılması itertools.chainiçin, daha hızlı her liste döngü ve elemanları tek tek ekleme daha itertoolsAyrıca bitiştirme önce liste halinde her Sorgu Kümesi dönüştürmek daha az bellek kullanır C'de uygulanır.

Artık ortaya çıkan listeyi örneğin tarihe göre sıralamak mümkündür (hasen j'nin başka bir cevaba yaptığı yorumda istendiği gibi). sorted()Fonksiyon elverişli bir jeneratör kabul eder ve bir liste döndürür:

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

Python 2.4 veya üstünü kullanıyorsanız attrgetterlambda yerine kullanabilirsiniz . Daha hızlı olduğunu okuduğumu hatırlıyorum, ancak bir milyon öğe listesi için fark edilir bir hız farkı görmedim.

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

14
Bir OR sorgusu gerçekleştirmek için sorgu kümelerini aynı tablodan birleştirirseniz ve yinelenen satırlarınız varsa, bunları groupby işleviyle ortadan kaldırabilirsiniz: from itertools import groupby unique_results = [rows.next() for (key, rows) in groupby(result_list, key=lambda obj: obj.id)]
Josh Russo

1
Pekala, bu bağlamda groupby fonksiyonu hakkında nm. Q işlevi ile ihtiyacınız olan herhangi bir VEYA sorgusunu gerçekleştirebilmeniz gerekir: https://docs.djangoproject.com/en/1.3/topics/db/queries/#complex-lookups-with-q-objects
Josh Russo

2
@apelliciari Zinciri list.extend öğesinden çok daha az bellek kullanır, çünkü her iki listeyi de tam olarak belleğe yüklemesi gerekmez.
Dan Gayle

2
@AWrightIV İşte bu bağlantının yeni sürümü: docs.djangoproject.com/en/1.8/topics/db/queries/…
Josh Russo

1
Bu yaklaşıyor ama var'list' object has no attribute 'complex_filter'
grillazz

466

Bunu dene:

matches = pages | articles | posts

İsterseniz order_byveya benzeri güzel olan querysetlerin tüm işlevlerini korur .

Lütfen dikkat: Bu, iki farklı modeldeki sorgu kümelerinde çalışmaz.


10
Yine de dilimlenmiş querysets üzerinde çalışmıyor. Yoksa bir şey mi kaçırıyorum?
sthzg

1
"|" Kullanarak sorgu kümelerine katılırdım ama her zaman iyi çalışmaz. "Q" kullanmak daha iyidir: docs.djangoproject.com/en/dev/topics/db/queries/…
Ignacio Pérez

1
Django 1.6 kullanarak kopyalar oluşturmuyor gibi görünüyor.
Teekin

15
İşte |set union operatörü, bitwise VEYA.
e100

6
@ e100 hayır, ayarlanan sendika operatörü değil. django bitseli
shangxiao

109

İlgili, aynı modeldeki sorgu kümelerini veya birkaç modeldeki benzer alanları karıştırmak için, Django 1.11 ile başlayarak bir qs.union()yöntem de mevcuttur:

union()

union(*other_qs, all=False)

Django 1.11'de yeni . İki veya daha fazla Sorgu Kümesinin sonuçlarını birleştirmek için SQL'in UNION işlecini kullanır. Örneğin:

>>> qs1.union(qs2, qs3)

UNION operatörü varsayılan olarak yalnızca farklı değerler seçer. Yinelenen değerlere izin vermek için all = True bağımsız değişkenini kullanın.

union (), intersection () ve fark (), argümanlar diğer modellerin QuerySets olsa bile, ilk QuerySet türünün model örneklerini döndürür. Farklı modelleri geçmek, SEÇİM listesi tüm Sorgu Kümelerinde aynı olduğu sürece çalışır (en azından türler, adlar aynı sıradaki türler kadar önemli değildir).

Ayrıca, elde edilen Sorgu Kümesinde yalnızca LIMIT, OFFSET ve ORDER BY (dilimleme ve order_by ()) işlevlerine izin verilir. Ayrıca, veritabanları birleştirilmiş sorgularda hangi işlemlere izin verildiğine kısıtlamalar getirir. Örneğin, çoğu veritabanı birleştirilmiş sorgularda LIMIT veya OFFSET öğesine izin vermez.

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


Bu, benzersiz değerlere sahip olması gereken sorun setim için daha iyi bir çözümdür.
Yanan Kristaller

Geodjango geometrileri için çalışmaz.
MarMat

Sendikayı nereden ithal ediyorsunuz? X sayısı sorgu kümesinden birinden mi gelmesi gerekiyor?
Jack

Evet, bir queryset yöntemidir.
Udi

Arama filtrelerini kaldırdığını düşünüyorum
Pierre Cordier

76

QuerySetChainAşağıdaki sınıfı kullanabilirsiniz . Django'nun sayfalandırıcısı ile birlikte kullanıldığında, veritabanına yalnızca COUNT(*)tüm SELECT()sorgu kümeleri için sorgularla ve yalnızca kayıtları geçerli sayfada görüntülenen sorgu kümeleri için sorgularla vurulmalıdır.

Zincirlenmiş sorgu kümelerinin tümü aynı modeli kullanıyor olsa bile, genel görünümlere sahip template_name=bir a kullanıp kullanmayacağınızı belirtmeniz gerektiğini unutmayın 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()

Örneğinizde, kullanım şöyle olacaktır:

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)

Ardından , örneğinizde matcheskullandığınız gibi sayfalayıcı ile kullanın result_list.

itertoolsO Django üzerinde çalışan tüm Python sürümlerinde kullanılabilir olduğundan bu modül, Python 2.3 kullanılmaya başlandı.


5
Güzel bir yaklaşım, ama burada gördüğüm bir sorun, sorgu kümeleri "kafa-kuyruk" eklenmesidir. Her bir sorgu kümesi tarihe göre sıralanırsa ve kombine kümenin tarihe göre sıralanması gerekiyorsa ne olur?
hasen

Bu kesinlikle umut verici görünüyor, harika, bunu denemek zorundayım, ama bugün zamanım yok. Sorunumu çözerse size geri döneceğim. Harika iş.
espenhogbakk

Tamam, bugün denemek zorunda kaldım, ama işe yaramadı, ilk önce _clone özniteliğine sahip olmadığından şikayet etti, bu yüzden bunu ekledim, sadece _all'i kopyaladı ve çalıştı, ancak sayfa oluşturucunun bu queryset ile ilgili bir sorunu var gibi görünüyor. Bu sayfa hatası alıyorum: "
bilinmeyen

1
@Espen Python kütüphanesi: pdb, loglama. Harici: IPython, ipdb, django-logging, django-debug-araç çubuğu, django-komut-uzantıları, werkzeug. Kodda print ifadeleri veya loglama modülünü kullanın. Her şeyden önce, kabukta içgözlem yapmayı öğrenin. Django'da hata ayıklama ile ilgili blog gönderileri için Google. Yardımcı olduğuma sevindim!
akaihola

4
@patrick bkz. djangosnippets.org/snippets/1103 ve djangosnippets.org/snippets/1933 - özellikle ikincisi çok kapsamlı bir çözümdür
akaihola

27

Mevcut yaklaşımınızın en büyük dezavantajı, yalnızca bir sayfa sayfasını görüntüleyecek olsanız bile, her seferinde tüm sonuç kümesini veritabanından çekmeniz gerektiğinden, büyük arama sonucu kümelerindeki verimsizliğidir.

Sadece gerçekten ihtiyacınız olan nesneleri veritabanından aşağı çekmek için, bir liste değil, bir Sorgu Kümesi'nde sayfalandırma kullanmanız gerekir. Bunu yaparsanız, Django aslında QuerySet'i sorgu yürütülmeden önce dilimler, böylece SQL sorgusu yalnızca gerçekte görüntüleyeceğiniz kayıtları almak için OFFSET ve LIMIT kullanır. Ancak, aramanızı bir şekilde tek bir sorguda sıkıştıramadıkça bunu yapamazsınız.

Üç modelinizin de başlık ve gövde alanlarına sahip olduğu düşünüldüğünde, neden model mirasını kullanmıyorsunuz ? Her üç modelin de başlık ve gövdeye sahip ortak bir atadan miras almasını ve aramayı ata modelinde tek bir sorgu olarak gerçekleştirmesini sağlayın.


23

Çok sayıda sorgu kümesini zincirlemek istiyorsanız, şunu deneyin:

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

burada: dokümanlar sorgu kümelerinin bir listesidir



8

Bu da iki şekilde gerçekleştirilebilir.

Bunu yapmanın ilk yolu

Queryset |için birleşim operatörünü kullanarak iki queryset birleşimini alın. Her iki queryset de aynı modele / tek modele aitse, senders operatörünü kullanarak queryset'leri birleştirmek mümkündür.

Bir örnek için

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

Bunu yapmanın 2. yolu

İki queryset arasındaki birleştirme işlemini gerçekleştirmenin bir başka yolu da itertools zincir fonksiyonunu kullanmaktır .

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

7

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

Birleştirmek querysetsve hala a ile çıkmak QuerySetistiyorsanız, django-queryset-dizisini kontrol etmek isteyebilirsiniz .

Ama bununla ilgili bir not. querysetsTartışma olarak sadece iki tane alır . Ancak python ile reduceher zaman birden fazla querysets'ye uygulayabilirsiniz .

from functools import reduce
from queryset_sequence import QuerySetSequence

combined_queryset = reduce(QuerySetSequence, list_of_queryset)

Ve bu kadar. Aşağıda ben koştum bir durumdur ve ben istihdam nasıl list comprehension, reducevedjango-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)Aynı şeyi yapmıyor mu ? Bunun geçerli bir kullanım örneği olduğundan emin değilim. Sanırım böyle bir şey yapmaya başlamak için önce Bookbirden fazla owners olması gerekebilir .
Will S

Evet aynı şeyi yapıyor. Denedim. Her neyse, belki de bu başka bir durumda faydalı olabilir. Bunu işaret ettiğiniz için teşekkürler. Yeni başlayanlar olarak tüm kısayolları bilmeye tam olarak başlamazsınız. Bazen karga sineğini takdir etmek için yük dolambaçlı yol seyahat etmek gerekir
chidimo

6

işte bir fikir ... sadece üçünün her birinden tam bir sonuç sayfası çekin ve en az 20 yararlı olanı atın ... bu büyük sorgu kümelerini ortadan kaldırır ve bu şekilde çok değil, sadece küçük bir performanstan ödün verirsiniz.


1

Bu işi başka kütüphaneler kullanmadan yapacak

result_list = list(page_list) + list(article_list) + list(post_list)

-1

Bu özyinelemeli işlev, sorgu kümesi dizisini bir sorgu kümesinde birleştirir.

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
Kelimenin tam anlamıyla kayboldum.
lycuid

biz sorgu sonucu birleştirmek çalışma zamanında kullanılamaz ve bunu yapmak gerçekten kötü bir fikir. çünkü bazen sonuç üzerine çoğaltma ekler.
Devang Hingu
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.