Django wybiera tylko wiersze ze zduplikowanymi wartościami pól


99

załóżmy, że mamy model w django zdefiniowany w następujący sposób:

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

Pole nazwy nie jest unikalne i dlatego może mieć zduplikowane wartości. Muszę wykonać następujące zadania: Wybierz wszystkie wiersze z modelu, które mają co najmniej jeden duplikat wartości w namepolu.

Wiem jak to zrobić używając zwykłego SQL (może nie być najlepszym rozwiązaniem):

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

Czy jest więc możliwe wybranie tego za pomocą django ORM? Albo lepsze rozwiązanie SQL?

Odpowiedzi:


198

Próbować:

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

To jest tak blisko, jak to tylko możliwe dzięki Django. Problem w tym, że to zwróci ValuesQuerySettylko namei count. Możesz jednak użyć tego do skonstruowania zwykłego QuerySet, przesyłając go z powrotem do innego zapytania:

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
Prawdopodobnie miałeś na myśli Literal.objects.values('name').annotate(name_count=Count('name')).filter(name_count__gt=1)?
dragoon

Oryginalne zapytanie podajeCannot resolve keyword 'id_count' into field
dragoon

2
Dzięki za zaktualizowaną odpowiedź, myślę, że zostanę przy tym rozwiązaniu, możesz to zrobić nawet bez zrozumienia listy za pomocąvalues_list('name', flat=True)
dragoon

1
Django wcześniej miał błąd w tym (mógł zostać naprawiony w ostatnich wersjach), gdzie jeśli nie określisz nazwy pola dla Countadnotacji do zapisania jako, domyślnie jest to [field]__count. Jednak składnia podwójnego podkreślenia jest również tym, jak Django interpretuje, że chcesz wykonać złączenie. Tak więc, zasadniczo, kiedy próbujesz to filtrować, Django myśli, że próbujesz zrobić połączenie, countktóre oczywiście nie istnieje. Rozwiązaniem jest określenie nazwy wyniku adnotacji, annotate(mycount=Count('id'))a następnie włączenie filtru mycount.
Chris Pratt

1
jeśli dodasz kolejne wywołanie do values('name')po wywołaniu adnotacji, możesz usunąć rozumienie listy i powiedzieć, Literal.objects.filter(name__in=dupes)które pozwoli to wszystko wykonać w jednym zapytaniu.
Piper Merriam

45

Zostało to odrzucone jako zmiana. Więc tutaj jest to lepsza odpowiedź

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

Zwróci to ValuesQuerySetze wszystkimi zduplikowanymi nazwami. Możesz jednak użyć tego do skonstruowania zwykłego QuerySet, przesyłając go z powrotem do innego zapytania. ORM django jest wystarczająco inteligentny, aby połączyć je w jedno zapytanie:

Literal.objects.filter(name__in=dups)

Dodatkowe wywołanie .values('name')po wywołaniu adnotacji wygląda trochę dziwnie. Bez tego podzapytanie kończy się niepowodzeniem. Dodatkowe wartości sprawiają, że ORM wybiera tylko kolumnę nazwy dla podzapytania.


Niezła sztuczka, niestety zadziała to tylko wtedy, gdy zostanie użyta tylko jedna wartość (np. Jeśli użyto zarówno „name”, jak i „phone”, ostatnia część nie zadziała).
guival

1
Do czego służy .order_by()?
stefanfoulis

4
@stefanfoulis Usuwa wszelkie istniejące zamówienia. Jeśli masz porządkowanie zestawu modeli, staje się to częścią GROUP BYklauzuli SQL , a to psuje. Odkryłem to, grając z Subquery (w którym robisz bardzo podobne grupowanie przez .values())
Oli

10

spróbuj użyć agregacji

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

Ok, to daje aktualną listę nazw, ale czy można jednocześnie wybrać identyfikatory i inne pola?
dragoon

@dragoon - nie, ale Chris Pratt omówił alternatywę w swojej odpowiedzi.
JamesO

5

Jeśli używasz PostgreSQL, możesz zrobić coś takiego:

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

Wynika z tego raczej proste zapytanie 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

Jeśli chcesz uzyskać tylko listę nazw, ale nie obiekty, możesz użyć następującego zapytania

repeated_names = Literal.objects.values('name').annotate(Count('id')).order_by().filter(id__count__gt=1).values_list('name', flat='true')
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.