web-dev-qa-db-fra.com

Django sélectionner uniquement les lignes avec des valeurs de champ en double

supposons que nous ayons un modèle en Django défini comme suit:

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

Le champ de nom n'est pas unique et peut donc avoir des valeurs en double. J'ai besoin d'accomplir la tâche suivante: Sélectionnez toutes les lignes du modèle qui ont au moins une valeur en double du champ name.

Je sais comment le faire en utilisant du SQL simple (ce n'est peut-être pas la meilleure solution):

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

Alors, est-il possible de sélectionner ceci en utilisant Django ORM? Ou une meilleure solution SQL?

82
dragoon

Essayer:

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

C'est aussi proche que possible avec Django. Le problème est que cela renverra un ValuesQuerySet avec seulement name et count. Cependant, vous pouvez ensuite l'utiliser pour construire un QuerySet normal en le réintroduisant dans une autre requête:

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])
166
Chris Pratt

Cela a été rejeté comme modification. Voici donc une réponse meilleure

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

Cela renverra un ValuesQuerySet avec tous les noms en double. Cependant, vous pouvez ensuite l'utiliser pour construire un QuerySet normal en le réintroduisant dans une autre requête. L'ORM Django est suffisamment intelligent pour les combiner en une seule requête:

Literal.objects.filter(name__in=dups)

L'appel supplémentaire à .values('name') après l'appel d'annotation semble un peu étrange. Sans cela, la sous-requête échoue. Les valeurs supplémentaires incitent l'ORM à sélectionner uniquement la colonne de nom pour la sous-requête.

36
Piper Merriam

essayez d'utiliser agrégation

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

Si vous utilisez PostgreSQL, vous pouvez faire quelque chose comme ceci:

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

Il en résulte cette requête SQL plutôt simple:

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
2
Eugene Pakhomov

Si vous souhaitez générer uniquement une liste de noms mais pas des objets, vous pouvez utiliser la requête suivante

repeated_names = Literal.objects.values('name').annotate(Count('id')).order_by().filter(id__count__gt=1).values_list('name', flat='true')
0
user2959723