Django เลือกเฉพาะแถวที่มีค่าฟิลด์ซ้ำกัน


99

สมมติว่าเรามีโมเดลใน django ที่กำหนดไว้ดังนี้:

class Literal:
    name = models.CharField(...)
    ...

ฟิลด์ชื่อไม่ซ้ำกันและอาจมีค่าที่ซ้ำกันได้ ฉันต้องการทำงานต่อไปนี้ให้สำเร็จ: เลือกแถวทั้งหมดจากโมเดลที่มีค่าที่ซ้ำกันอย่างน้อยหนึ่งค่าของnameฟิลด์

ฉันรู้วิธีทำโดยใช้ SQL ธรรมดา (อาจไม่ใช่วิธีแก้ปัญหาที่ดีที่สุด):

select * from literal where name IN (
    select name from literal group by name having count((name)) > 1
);

ดังนั้นเป็นไปได้ไหมที่จะเลือกสิ่งนี้โดยใช้ django ORM? หรือโซลูชัน SQL ที่ดีกว่า?

คำตอบ:


201

ลอง:

from django.db.models import Count
Literal.objects.values('name')
               .annotate(Count('id')) 
               .order_by()
               .filter(id__count__gt=1)

ใกล้ที่สุดเท่าที่จะทำได้กับ Django ปัญหาคือสิ่งนี้จะส่งคืน a ที่ValuesQuerySetมีเพียงnameและcount. อย่างไรก็ตามคุณสามารถใช้สิ่งนี้เพื่อสร้างแบบปกติQuerySetโดยป้อนกลับเข้าไปในแบบสอบถามอื่น:

dupes = Literal.objects.values('name')
                       .annotate(Count('id'))
                       .order_by()
                       .filter(id__count__gt=1)
Literal.objects.filter(name__in=[item['name'] for item in dupes])

5
คุณอาจหมายถึงLiteral.objects.values('name').annotate(name_count=Count('name')).filter(name_count__gt=1)?
dragoon

ข้อความค้นหาเดิมให้Cannot resolve keyword 'id_count' into field
dragoon

2
ขอบคุณสำหรับคำตอบที่อัปเดตฉันคิดว่าฉันจะใช้วิธีแก้ปัญหานี้คุณสามารถทำได้โดยไม่ต้องเข้าใจรายการโดยใช้values_list('name', flat=True)
dragoon

1
ก่อนหน้านี้ Django มีข้อบกพร่องในเรื่องนี้ (อาจได้รับการแก้ไขแล้วในเวอร์ชันล่าสุด) ซึ่งหากคุณไม่ระบุชื่อฟิลด์สำหรับCountคำอธิบายประกอบที่จะบันทึกเป็นค่าเริ่มต้นจะ[field]__countเป็น อย่างไรก็ตามไวยากรณ์การขีดล่างสองครั้งนั้นเป็นวิธีที่ Django ตีความว่าคุณต้องการเข้าร่วม ดังนั้นโดยพื้นฐานแล้วเมื่อคุณพยายามกรองสิ่งนั้น Django คิดว่าคุณกำลังพยายามเข้าร่วมcountซึ่งเห็นได้ชัดว่าไม่มีอยู่จริง การแก้ไขคือการระบุชื่อสำหรับผลลัพธ์คำอธิบายประกอบของคุณกล่าวคือannotate(mycount=Count('id'))แล้วกรองmycountแทน
Chris Pratt

1
หากคุณเพิ่มการโทรอีกครั้งvalues('name')หลังจากการเรียกเพื่อใส่คำอธิบายประกอบคุณสามารถลบความเข้าใจของรายการและพูดLiteral.objects.filter(name__in=dupes)สิ่งที่จะทำให้สามารถดำเนินการทั้งหมดนี้ได้ในแบบสอบถามเดียว
Piper Merriam

45

สิ่งนี้ถูกปฏิเสธเป็นการแก้ไข ดังนั้นนี่คือคำตอบที่ดีกว่า

dups = (
    Literal.objects.values('name')
    .annotate(count=Count('id'))
    .values('name')
    .order_by()
    .filter(count__gt=1)
)

สิ่งนี้จะส่งคืน a ValuesQuerySetพร้อมกับชื่อที่ซ้ำกันทั้งหมด อย่างไรก็ตามคุณสามารถใช้สิ่งนี้เพื่อสร้างแบบปกติQuerySetโดยป้อนกลับเข้าไปในแบบสอบถามอื่น django ORM ฉลาดพอที่จะรวมสิ่งเหล่านี้ไว้ในแบบสอบถามเดียว:

Literal.objects.filter(name__in=dups)

การเรียกพิเศษ.values('name')หลังจากการเรียกคำอธิบายประกอบดูแปลก ๆ เล็กน้อย หากไม่มีสิ่งนี้แบบสอบถามย่อยจะล้มเหลว ค่าพิเศษจะหลอกให้ ORM เลือกเฉพาะคอลัมน์ชื่อสำหรับเคียวรีย่อย


เคล็ดลับที่ดีน่าเสียดายที่สิ่งนี้จะใช้ได้ก็ต่อเมื่อมีการใช้เพียงค่าเดียวเท่านั้น (เช่นหากใช้ทั้ง 'ชื่อ' และ 'โทรศัพท์' ที่ใช้ส่วนสุดท้ายจะไม่ทำงาน)
guival

1
มีไว้.order_by()เพื่ออะไร?
stefanfoulis

4
@stefanfoulis จะล้างคำสั่งซื้อที่มีอยู่ออกไป หากคุณมีการสั่งซื้อชุดโมเดลสิ่งนี้จะกลายเป็นส่วนหนึ่งของส่วนGROUP BYคำสั่งSQL และจะแบ่งสิ่งต่างๆ พบว่าเมื่อเล่นกับ Subquery (ซึ่งคุณจัดกลุ่มคล้ายกันมากผ่าน.values())
Oli

10

ลองใช้การรวม

Literal.objects.values('name').annotate(name_count=Count('name')).exclude(name_count=1)

ตกลงให้รายชื่อที่ถูกต้อง แต่สามารถเลือกรหัสและช่องอื่น ๆ พร้อมกันได้หรือไม่?
dragoon

@dragoon - ไม่ แต่ Chris Pratt ได้กล่าวถึงทางเลือกในคำตอบของเขา
JamesO

5

ในกรณีที่คุณใช้ PostgreSQL คุณสามารถทำสิ่งนี้ได้:

from django.contrib.postgres.aggregates import ArrayAgg
from django.db.models import Func, Value

duplicate_ids = (Literal.objects.values('name')
                 .annotate(ids=ArrayAgg('id'))
                 .annotate(c=Func('ids', Value(1), function='array_length'))
                 .filter(c__gt=1)
                 .annotate(ids=Func('ids', function='unnest'))
                 .values_list('ids', flat=True))

ผลลัพธ์ในแบบสอบถาม SQL ที่ค่อนข้างเรียบง่ายนี้:

SELECT unnest(ARRAY_AGG("app_literal"."id")) AS "ids"
FROM "app_literal"
GROUP BY "app_literal"."name"
HAVING array_length(ARRAY_AGG("app_literal"."id"), 1) > 1

0

ถ้าคุณต้องการผลลัพธ์เฉพาะรายการชื่อ แต่ไม่ใช่วัตถุคุณสามารถใช้แบบสอบถามต่อไปนี้

repeated_names = Literal.objects.values('name').annotate(Count('id')).order_by().filter(id__count__gt=1).values_list('name', flat='true')
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.