Django ne sélectionne que les lignes avec des valeurs de champ en double

96

supposons que nous ayons un modèle dans django défini comme suit:

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

Le champ Nom n'est pas unique et peut donc avoir des valeurs en double. Je dois accomplir la tâche suivante: Sélectionnez toutes les lignes du modèle qui ont au moins une valeur en double du namechamp.

Je sais comment le faire en utilisant SQL brut (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?

dragon
la source

Réponses:

193

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 retournera un ValuesQuerySetavec seulement nameet count. Cependant, vous pouvez ensuite l'utiliser pour construire un régulier QuerySeten le réinjectant 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])
Chris Pratt
la source
5
Vous avez probablement voulu dire Literal.objects.values('name').annotate(name_count=Count('name')).filter(name_count__gt=1)?
dragoon
Requête originale donneCannot resolve keyword 'id_count' into field
dragoon
2
Merci pour la réponse mise à jour, je pense que je vais m'en tenir à cette solution, vous pouvez même le faire sans compréhension de la liste en utilisantvalues_list('name', flat=True)
dragoon
1
Django avait auparavant un bogue à ce sujet (peut-être avoir été corrigé dans les versions récentes) où si vous ne spécifiez pas de nom de champ pour l' Countannotation à enregistrer sous, il est par défaut [field]__count. Cependant, cette syntaxe à double trait de soulignement est également la façon dont Django interprète votre souhait de faire une jointure. Donc, essentiellement lorsque vous essayez de filtrer sur cela, Django pense que vous essayez de faire une jointure avec countlaquelle il n'existe manifestement pas. Le correctif consiste à spécifier un nom pour le résultat de votre annotation, c'est annotate(mycount=Count('id'))-à- dire puis à filtrer à la mycountplace.
Chris Pratt
1
si vous ajoutez un autre appel values('name')après votre appel à annoter, vous pouvez supprimer la compréhension de la liste et dire Literal.objects.filter(name__in=dupes)ce qui permettra à tout cela d'être exécuté en une seule requête.
Piper Merriam
43

Cela a été rejeté en tant que modification. Alors voici une meilleure réponse

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

Cela renverra un ValuesQuerySetavec tous les noms en double. Cependant, vous pouvez ensuite l'utiliser pour construire un régulier QuerySeten le réinjectant 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 à ne sélectionner que la colonne de nom pour la sous-requête.

Piper Merriam
la source
Belle astuce, malheureusement cela ne fonctionnera que si une seule valeur est utilisée (par exemple, si 'nom' et 'téléphone' étaient tous les deux utilisés, la dernière partie ne fonctionnerait pas).
guival le
1
Quel est le .order_by()pour?
stefanfoulis
4
@stefanfoulis Il efface toute commande existante. Si vous avez un ordre d'ensemble de modèles, cela devient une partie de la GROUP BYclause SQL , et cela brise les choses. J'ai découvert cela en jouant avec Subquery (dans lequel vous faites un regroupement très similaire via .values())
Oli
10

essayez d'utiliser l' agrégation

Literal.objects.values('name').annotate(name_count=Count('name')).exclude(name_count=1)
JamesO
la source
Ok, cela donne la liste correcte des noms, mais est-il possible de sélectionner des identifiants et d'autres champs en même temps?
dragoon
@dragoon - non mais Chris Pratt a couvert l'alternative dans sa réponse.
JamesO
5

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 assez 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
Eugène Pakhomov
la source
0

Si vous souhaitez obtenir uniquement une liste de noms mais pas d'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')
user2959723
la source