วิธีรวมแบบสอบถามสองชุดขึ้นไปในมุมมอง Django


654

ฉันพยายามสร้างการค้นหาเว็บไซต์ Django ที่ฉันกำลังสร้างและในการค้นหานั้นฉันกำลังค้นหาใน 3 แบบที่แตกต่างกัน และเพื่อให้ได้เลขหน้าในรายการผลการค้นหาฉันต้องการใช้มุมมอง object_list ทั่วไปเพื่อแสดงผลลัพธ์ แต่การทำเช่นนั้นฉันต้องรวม 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?


ดูเหมือนว่า t_rybik ได้สร้างโซลูชันที่ครอบคลุมที่djangosnippets.org/snippets/1933
akaihola

สำหรับการค้นหาจะดีกว่าที่จะใช้โซลูชันเฉพาะเช่นHaystackซึ่งมีความยืดหยุ่นสูง
ผู้ดูแล

1
ผู้ใช้ Django 1.11 และ abv ดูคำตอบนี้ - stackoverflow.com/a/42186970/6003362
Sahil Agarwal

หมายเหตุ : คำถามถูก จำกัด ในกรณีที่หายากมากเมื่อรวม 3 รุ่นที่แตกต่างกันเข้าด้วยกันคุณไม่จำเป็นต้องแยกโมเดลอีกครั้งในรายชื่อเพื่อแยกความแตกต่างของข้อมูลตามประเภท สำหรับกรณีส่วนใหญ่ - หากคาดว่าจะมีความแตกต่าง - จะมีส่วนต่อประสานที่ผิด สำหรับรุ่นเดียวกัน: unionเห็นคำตอบเกี่ยวกับ
Sławomir Lenart

คำตอบ:


1058

การต่อคิวรีเซ็ตเข้ากับรายการเป็นวิธีที่ง่ายที่สุด หากฐานข้อมูลจะได้รับผลกระทบจากชุดค้นหาทั้งหมดอยู่แล้ว (เช่นเนื่องจากต้องเรียงลำดับผลลัพธ์) จะไม่เพิ่มต้นทุนเพิ่มเติม

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

การใช้itertools.chainเร็วกว่าการวนลูปแต่ละรายการและต่อท้ายองค์ประกอบทีละตัวเนื่องจากitertoolsมีการนำไปใช้ใน C และยังใช้หน่วยความจำน้อยกว่าการแปลงแต่ละชุดคิวรีเป็นรายการก่อนที่จะต่อกัน

ตอนนี้มันเป็นไปได้ที่จะเรียงลำดับรายการผลลัพธ์เช่นตามวันที่ (ตามที่ร้องขอในความคิดเห็นของ 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'))

14
หากการรวมชุดข้อความจากตารางเดียวกันการดำเนินการหรือการสอบถามและมีแถวที่ซ้ำกันคุณสามารถกำจัดพวกเขาด้วยฟังก์ชั่น GroupBy นี้: from itertools import groupby unique_results = [rows.next() for (key, rows) in groupby(result_list, key=lambda obj: obj.id)]
จอชรัสเซีย

1
ตกลงดังนั้นเกี่ยวกับฟังก์ชัน groupby ในบริบทนี้ ด้วยฟังก์ชั่น Q คุณควรจะสามารถดำเนินการหรือสอบถามที่คุณต้องการ: https://docs.djangoproject.com/en/1.3/topics/db/queries/#complex-lookups-with-q-objects
Josh Russo

2
@apelliciari Chain ใช้หน่วยความจำน้อยกว่า list.extend อย่างมากเนื่องจากไม่จำเป็นต้องโหลดทั้งสองรายการในหน่วยความจำ
Dan Gayle

2
@AWrightIV ต่อไปนี้เป็นเวอร์ชันใหม่ของลิงก์นั้น: docs.djangoproject.com/en/1.8/topics/db/queries/ ......
Josh Russo

1
พยายาม approacg นี้ แต่มี'list' object has no attribute 'complex_filter'
grillazz

466

ลองสิ่งนี้:

matches = pages | articles | posts

มันยังคงฟังก์ชั่นทั้งหมดของชุดแบบสอบถามที่ดีถ้าคุณต้องการorder_byหรือคล้ายกัน

โปรดทราบ:วิธีนี้ใช้ไม่ได้กับชุดการสืบค้นจากสองแบบที่แตกต่างกัน


10
ไม่สามารถใช้การสืบค้นคิวรีแบบแบ่งเป็นชุดได้ หรือฉันกำลังพลาดอะไรอยู่?
sthzg

1
ฉันเคยเข้าร่วมชุดสืบค้นข้อมูลโดยใช้ "|" แต่ก็ไม่ได้ผลดีเสมอไป ควรใช้ "Q": docs.djangoproject.com/en/dev/topics/db/queries/?hl=th
Ignacio Pérez

1
ดูเหมือนจะไม่สร้างรายการซ้ำโดยใช้ Django 1.6
Teekin

15
นี่|คือโอเปอเรเตอร์การตั้งค่าสหภาพไม่ใช่ค่าบิตหรือ
e100

6
@ e100 ไม่ไม่ใช่ตัวดำเนินการสหภาพที่ตั้งค่าไว้ django โหลดตัวดำเนินการ bitwise OR: github.com/django/django/blob/master/django/db/models/?hl=th
shangxiao

109

ที่เกี่ยวข้องสำหรับการผสมชุดข้อความจากรุ่นเดียวกันหรือคล้ายกันสำหรับเขตข้อมูลจากไม่กี่รุ่นที่เริ่มต้นด้วยDjango 1.11 qs.union()วิธีนอกจากนี้ยังมี:

union()

union(*other_qs, all=False)

ใหม่ใน Django 1.11 ใช้ตัวดำเนินการ UNION ของ SQL เพื่อรวมผลลัพธ์ของ QuerySets สองชุดขึ้นไป ตัวอย่างเช่น:

>>> qs1.union(qs2, qs3)

ผู้ประกอบการ UNION เลือกเฉพาะค่าที่แตกต่างโดยค่าเริ่มต้น หากต้องการอนุญาตให้ใช้ค่าซ้ำกันให้ใช้อาร์กิวเมนต์ all = True

union (), intersection (), และ difference () คืนค่าอินสแตนซ์โมเดลของชนิดของ QuerySet แรกแม้ว่าอาร์กิวเมนต์จะเป็น QuerySets ของโมเดลอื่น การส่งแบบจำลองที่แตกต่างกันจะทำงานได้นานเท่าที่รายการ SELECT จะเหมือนกันใน QuerySets ทั้งหมด (อย่างน้อยก็ประเภทชื่อนั้นไม่สำคัญเท่ากับชนิดในลำดับเดียวกัน)

นอกจากนี้อนุญาตให้ใช้ 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


นี่เป็นทางออกที่ดีกว่าสำหรับชุดปัญหาของฉันที่ต้องมีค่าที่ไม่ซ้ำกัน
Burning Crystals

ไม่ทำงานกับรูปทรงเรขาคณิตของ geodjango
MarMat

คุณนำเข้าสหภาพจากที่ไหน มันต้องมาจากหนึ่งในจำนวนชุดแบบสอบถามหรือไม่?
แจ็ค

ใช่มันเป็นวิธีการของชุดแบบสอบถาม
Udi

ฉันคิดว่ามันลบตัวกรองการค้นหา
Pierre Cordier

76

คุณสามารถใช้QuerySetChainคลาสด้านล่าง เมื่อใช้กับ paginator ของ Django ก็ควรกดฐานข้อมูลพร้อมกับCOUNT(*)แบบสอบถามสำหรับชุดSELECT()แบบสอบถามทั้งหมดและแบบสอบถามเฉพาะสำหรับชุดแบบสอบถามเหล่านั้นที่มีระเบียนที่จะแสดงในหน้าปัจจุบัน

โปรดทราบว่าคุณต้องระบุtemplate_name=ว่าใช้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()

ในตัวอย่างของคุณการใช้งานจะเป็น:

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)

จากนั้นใช้matchesกับเครื่องมือจัดเรียงอย่างที่คุณใช้result_listในตัวอย่างของคุณ

itertoolsโมดูลได้รับการแนะนำในหลาม 2.3 ดังนั้นจึงควรจะมีในทุกรุ่น Python Django ทำงานบน


5
วิธีการที่ดี แต่ปัญหาหนึ่งที่ฉันเห็นที่นี่คือชุดแบบสอบถามถูกผนวก "หัวต่อท้าย" จะทำอย่างไรถ้าแต่ละชุดคิวรีเรียงลำดับตามวันที่และหนึ่งชุดต้องการชุดรวมที่จะเรียงลำดับตามวันที่ด้วย
hasen

นี่มันช่างดูดีมากฉันจะต้องลองดู แต่วันนี้ฉันไม่มีเวลา ฉันจะกลับไปหาคุณถ้ามันแก้ปัญหาของฉัน การทำงานที่ดี.
espenhogbakk

ตกลงฉันต้องลองวันนี้ แต่มันไม่ได้ผลก่อนอื่นก็บ่นว่ามันไม่จำเป็นต้องใช้ _clone คุณลักษณะดังนั้นฉันจึงเพิ่มที่หนึ่งเพียงคัดลอก _all และที่ใช้งานได้ แต่ดูเหมือนว่า paginator มีปัญหาบางอย่างกับชุดแบบสอบถามนี้ ฉันได้รับข้อผิดพลาด paginator นี้: "len () ของวัตถุ unsized"
espenhogbakk

1
@Espen Python library: pdb, logging ภายนอก: IPython, ipdb, django-logging, django-debug-toolbar, django-command-extensions, werkzeug ใช้คำสั่งพิมพ์ในรหัสหรือใช้โมดูลการบันทึก เหนือสิ่งอื่นใดเรียนรู้ที่จะใคร่ครวญในเปลือก Google สำหรับบล็อกโพสต์เกี่ยวกับการดีบัก Django ดีใจที่ได้ช่วยเหลือ!
akaihola

4
@patrick ดูdjangosnippets.org/snippets/1103และdjangosnippets.org/snippets/1933 - โดยเฉพาะอย่างยิ่งหลังเป็นโซลูชันที่ครอบคลุมมาก
akaihola

27

ข้อเสียใหญ่ของวิธีการปัจจุบันของคุณคือความไม่มีประสิทธิภาพกับชุดผลลัพธ์การค้นหาขนาดใหญ่เนื่องจากคุณต้องดึงชุดผลลัพธ์ทั้งหมดจากฐานข้อมูลในแต่ละครั้งแม้ว่าคุณต้องการแสดงผลลัพธ์เพียงหน้าเดียว

เพื่อดึงวัตถุที่คุณต้องการจริง ๆ จากฐานข้อมูลเท่านั้นคุณต้องใช้การแบ่งหน้าใน QuerySet ไม่ใช่รายการ หากคุณทำเช่นนี้ Django จะตัด QuerySet จริงก่อนที่จะดำเนินการค้นหาดังนั้นแบบสอบถาม SQL จะใช้ OFFSET และ LIMIT เพื่อรับเฉพาะระเบียนที่คุณจะแสดงจริง ๆ เท่านั้น แต่คุณไม่สามารถทำสิ่งนี้ได้หากคุณไม่สามารถยัดคำค้นหาของคุณให้เป็นคำค้นหาเดียว

ระบุว่าทั้งสามรุ่นที่ท่านมีชื่อเรื่องและเนื้อหาเขตทำไมไม่ใช้มรดกรูปแบบ ? เพียงแค่มีทั้งสามรุ่นที่สืบทอดมาจากบรรพบุรุษร่วมที่มีชื่อเรื่องและเนื้อหาและทำการค้นหาในรูปแบบการสืบค้นเดียวกับแบบจำลองบรรพบุรุษ


23

ในกรณีที่คุณต้องการเชื่อมโยงแบบสอบถามจำนวนมากลองทำสิ่งนี้:

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

โดยที่: docs เป็นรายการชุดสืบค้น


16
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 เห็นอเล็กซ์ Gaynor


8

ซึ่งสามารถทำได้สองวิธีเช่นกัน

วิธีแรกในการทำเช่นนี้

ใช้ตัวดำเนินการ union สำหรับชุดแบบสอบถาม|เพื่อรวมแบบสอบถามสองชุด ถ้าทั้งชุดแบบสอบถามเป็นของรุ่นเดียวกัน / รุ่นเดียวกว่าที่เป็นไปได้ที่จะรวมชุดแบบสอบถามโดยใช้ตัวดำเนินการสหภาพ

ตัวอย่างเช่น

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 ในการทำเช่นนี้

อีกวิธีหนึ่งในการรวมการดำเนินการระหว่างสองชุดแบบสอบถามคือการใช้ฟังก์ชันลูกโซ่itertools

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

7

ข้อกำหนด: Django==2.0.2 ,django-querysetsequence==0.8

ในกรณีที่คุณต้องการที่จะรวมquerysetsและยังคงออกมาด้วยQuerySetคุณอาจต้องการที่จะตรวจสอบDjango-queryset ลำดับ

แต่ข้อสังเกตหนึ่งเกี่ยวกับมัน ใช้เวลาเพียงสองquerysetsเป็นอาร์กิวเมนต์ แต่ด้วยหลามreduceคุณสามารถนำไปใช้กับหลายquerysets

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

1
ไม่Book.objects.filter(owner__mentor=mentor)ทำสิ่งเดียวกัน ฉันไม่แน่ใจว่านี่เป็นกรณีใช้งานที่ถูกต้อง ฉันคิดว่าBookอาจต้องมีหลายowners ก่อนที่คุณจะต้องเริ่มทำสิ่งนี้
จะ S

ใช่มันทำสิ่งเดียวกัน ฉันลองแล้ว อย่างไรก็ตามอาจเป็นประโยชน์ในบางสถานการณ์ ขอบคุณสำหรับการชี้ให้เห็นว่า คุณไม่ทราบแน่ชัดว่าเป็นทางลัดทั้งหมดในฐานะผู้เริ่มต้น บางครั้งคุณต้องเดินทางไปตามถนนที่คดเคี้ยวเพื่อชื่นชมแมลงวันอีกา
chidimo

6

นี่คือแนวคิด ... เพียงดึงผลลัพธ์หนึ่งหน้าเต็มหน้าจากสามหน้าลงไปแล้วโยน 20 รายการที่มีประโยชน์น้อยที่สุด ... นี่เป็นการกำจัดชุดแบบสอบถามขนาดใหญ่และวิธีที่คุณเสียสละประสิทธิภาพเพียงเล็กน้อยแทนที่จะเป็นจำนวนมาก



-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
ฉันหลงทาง
lycuid

เรารวมผลลัพธ์ข้อความค้นหาที่ไม่สามารถใช้งานได้ในขณะใช้งานและเป็นความคิดที่ไม่ดีจริงๆ เพราะบางครั้งมันเพิ่มการซ้ำซ้อนเหนือผลลัพธ์
Devang Hingu
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.