87 votes

Dans un QuerySet Django, comment filtrer pour "not exists" dans une relation many-to-one ?

Je possède deux modèles de ce type :

class User(models.Model):
    email = models.EmailField()

class Report(models.Model):
    user = models.ForeignKey(User)

En réalité, chaque modèle possède d'autres champs qui n'ont pas d'importance pour cette question.

Je veux filtrer tous les utilisateurs dont l'adresse électronique commence par 'a' et qui n'ont pas de rapports. Il y aura plus de .filter() y .exclude() critères basés sur d'autres champs.

Je veux l'aborder comme ça :

users = User.objects.filter(email__like = 'a%')

users = users.filter(<other filters>)

users = ???

J'aimerais que l'on puisse filtrer les utilisateurs qui n'ont pas de rapports associés à eux. Comment dois-je m'y prendre ? Si ce n'est pas possible tel que je l'ai présenté, quelle est l'autre approche possible ?

1voto

Lukasz Koziara Points 2052

Pour filtrer les utilisateurs qui n'ont pas de rapports associés à eux, essayez ceci :

users = User.objects.exclude(id__in=[elem.user.id for elem in Report.objects.all()])

1voto

Chris Points 135

La meilleure option pour trouver des rangées où il y a es une ligne de jonction :

Report.objects.filter(user__isnull=False).distinct()

Il utilise un INNER JOIN (et vérifie ensuite de manière redondante User.id n'est pas nulle).

La meilleure option pour trouver des rangées où il y a des pas une ligne de jonction :

Report.objects.filter(user__isnull=True)

Cela rend LEFT OUTER JOIN puis vérifie User.id n'est pas nulle.

Les requêtes basées sur les jointures seront plus rapides que les sous-requêtes, ce qui est plus rapide que les nouvelles options disponibles, comme dans Django >= 3, pour trouver des lignes. sans une ligne de jonction :

Report.objects.filter(~Exists(User.objects.filter(report=OuterRef('pk'))))

Cela crée un WHERE NOT EXISTS (SELECT .. FROM User..) implique donc un ensemble de résultats intermédiaires potentiellement important (merci @Tomasz Gandor).

Ceci pour Django <3, où filter() ne peut pas recevoir de sous-requêtes, utilise également une sous-requête et est donc plus lent :

Report.objects.annotate(
    no_users=~Exists(User.objects.filter(report=OuterRef('pk')))
).filter(no_users=True)

Cela peut être combiné avec des sous-requêtes. Dans cet exemple, une Textbook a un certain nombre de Versions (c'est-à-dire, version a textbook_id ), et un version a un certain nombre de Pages (c'est-à-dire, page a version_id ). La sous-requête obtient la dernière version de chaque manuel scolaire auquel sont associées des pages :

subquery = (
    Version.objects
        .filter(
            # OuterRef joins to Version.textbook in outer query below
            textbook=OuterRef('textbook'), 
            # excludes rows with no joined Page records
            page__isnull=False)
        # ordered so [:1] below gets highest (ie, latest) version number
        .order_by('-number').distinct()
)
# Only the Version.ids of the latest versions that have pages returned by the subquery
books = Version.objects.filter(pk=Subquery(subquery.values('pk')[:1])).distinct()

Pour renvoyer les lignes qui ont une jointure avec l'une ou les deux tables, utilisez les objets Q ( Page y TextMarkup tous deux ont des clés étrangères annulables qui rejoignent File ):

from django.db.models import Q

File.objects.filter(Q(page__isnull=False) | Q(textmarkup__isnull=False).distinct()

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X