web-dev-qa-db-fra.com

Dans un Django QuerySet, comment filtrer pour "n'existe pas" dans une relation plusieurs-à-un

J'ai deux modèles comme celui-ci:

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

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

En réalité, chaque modèle a plus de domaines qui n'ont aucune conséquence à cette question.

Je souhaite filtrer tous les utilisateurs qui ont un e-mail commençant par "a" et n'ayant aucun rapport. Il y aura plus de critères .filter() et .exclude() basés sur d'autres champs.

Je veux l'approcher comme ceci:

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

users = users.filter(<other filters>)

users = ???

J'aimerais ??? pour filtrer les utilisateurs auxquels aucun rapport n'est associé. Comment pourrais-je faire ça? Si cela n'est pas possible comme je l'ai présenté, quelle est une approche alternative?

50
Krystian Cybulski

Utilisez isnull .

users_without_reports = User.objects.filter(report__isnull=True)
users_with_reports = User.objects.filter(report__isnull=False).distinct()

Lorsque vous utilisez isnull=False, La distinct() est requise pour éviter les résultats en double.

81
Alasdair

Nouveau dans Django 1.11 vous pouvez ajouter des sous-requêtes EXISTS:

User.objects.annotate(
    no_reports=~Exists(Reports.objects.filter(user__eq=OuterRef('pk')))
).filter(
    email__startswith='a',
    no_reports=True
)

Cela génère quelque chose de SQL comme ceci:

SELECT
    user.pk,
    user.email,
    NOT EXISTS (SELECT U0.pk FROM reports U0 WHERE U0.user = user.pk) AS no_reports
FROM user
WHERE email LIKE 'a%' AND NOT EXISTS (SELECT U0.pk FROM reports U0 WHERE U0.user = user.pk);

Une clause NOT EXISTS Est presque toujours le moyen le plus efficace de faire un filtre "n'existe pas".

Une fois # 25367 publié, vous pourrez utiliser ~Exists() directement dans un .filter(), en évitant la clause en double.

19
OrangeDog

La seule façon d'obtenir des SQL EXISTS/NOT EXISTS natifs sans requêtes supplémentaires ou JOIN est de l'ajouter en tant que SQL brut dans la clause .extra ():

users = users.extra(where=[
    """NOT EXISTS(SELECT 1 FROM {reports} 
                  WHERE user_id={users}.id)
    """.format(reports=Report._meta.db_table, users=User._meta.db_table)
])

En fait, c'est une solution assez évidente et efficace et je me demande parfois pourquoi elle n'a pas été intégrée à Django comme une recherche. Elle permet également d'affiner la sous-requête pour trouver par exemple uniquement les utilisateurs avec [ out] un rapport pendant la semaine dernière, ou avec [out] un rapport sans réponse/sans consultation.

11
Yuri Shatrov

la réponse d'Alasdair est utile, mais je n'aime pas utiliser distinct(). Cela peut parfois être utile, mais c'est généralement une odeur de code vous indiquant que vous avez gâché vos jointures.

Heureusement, Django's queryset vous permet de filtrer les sous-requêtes.

Voici quelques façons d'exécuter les requêtes à partir de votre question:

# Tested with Django 1.9.2
import logging
import sys

import Django
from Django.apps import apps
from Django.apps.config import AppConfig
from Django.conf import settings
from Django.db import connections, models, DEFAULT_DB_ALIAS
from Django.db.models.base import ModelBase

NAME = 'udjango'


def main():

    setup()

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

        def __repr__(self):
            return 'User({!r})'.format(self.email)

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

    syncdb(User)
    syncdb(Report)

    anne = User.objects.create(email='[email protected]')
    User.objects.create(email='[email protected]')
    alice = User.objects.create(email='[email protected]')
    User.objects.create(email='[email protected]')

    Report.objects.create(user=anne)
    Report.objects.create(user=alice)
    Report.objects.create(user=alice)

    logging.info('users without reports')
    logging.info(User.objects.filter(report__isnull=True, email__startswith='a'))

    logging.info('users with reports (allows duplicates)')
    logging.info(User.objects.filter(report__isnull=False, email__startswith='a'))

    logging.info('users with reports (no duplicates)')
    logging.info(User.objects.exclude(report__isnull=True).filter(email__startswith='a'))

    logging.info('users with reports (no duplicates, simpler SQL)')
    report_user_ids = Report.objects.values('user_id')
    logging.info(User.objects.filter(id__in=report_user_ids, email__startswith='a'))

    logging.info('Done.')


def setup():
    db_file = NAME + '.db'
    with open(db_file, 'w'):
        pass  # wipe the database
    settings.configure(
        DEBUG=True,
        DATABASES={
            DEFAULT_DB_ALIAS: {
                'ENGINE': 'Django.db.backends.sqlite3',
                'NAME': db_file}},
        LOGGING={'version': 1,
                 'disable_existing_loggers': False,
                 'formatters': {
                    'debug': {
                        'format': '%(asctime)s[%(levelname)s]'
                                  '%(name)s.%(funcName)s(): %(message)s',
                        'datefmt': '%Y-%m-%d %H:%M:%S'}},
                 'handlers': {
                    'console': {
                        'level': 'DEBUG',
                        'class': 'logging.StreamHandler',
                        'formatter': 'debug'}},
                 'root': {
                    'handlers': ['console'],
                    'level': 'INFO'},
                 'loggers': {
                    "Django.db": {"level": "DEBUG"}}})
    app_config = AppConfig(NAME, sys.modules['__main__'])
    apps.populate([app_config])
    Django.setup()
    original_new_func = ModelBase.__new__

    # noinspection PyDecorator
    @staticmethod
    def patched_new(cls, name, bases, attrs):
        if 'Meta' not in attrs:
            class Meta:
                app_label = NAME
            attrs['Meta'] = Meta
        return original_new_func(cls, name, bases, attrs)
    ModelBase.__new__ = patched_new


def syncdb(model):
    """ Standard syncdb expects models to be in reliable locations.

    Based on https://github.com/Django/django/blob/1.9.3
    /Django/core/management/commands/migrate.py#L285
    """
    connection = connections[DEFAULT_DB_ALIAS]
    with connection.schema_editor() as editor:
        editor.create_model(model)

main()

Si vous mettez cela dans un fichier Python et l'exécutez, vous devriez voir quelque chose comme ceci:

2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) PRAGMA foreign_keys; args=None
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) PRAGMA foreign_keys = 0; args=None
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) BEGIN; args=None
2017-10-06 09:56:22[DEBUG]Django.db.backends.schema.execute(): CREATE TABLE "udjango_user" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "email" varchar(254) NOT NULL); (params None)
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) CREATE TABLE "udjango_user" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "email" varchar(254) NOT NULL); args=None
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) PRAGMA foreign_keys = 0; args=None
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) PRAGMA foreign_keys; args=None
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) PRAGMA foreign_keys = 0; args=None
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) BEGIN; args=None
2017-10-06 09:56:22[DEBUG]Django.db.backends.schema.execute(): CREATE TABLE "udjango_report" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "user_id" integer NOT NULL REFERENCES "udjango_user" ("id")); (params None)
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) CREATE TABLE "udjango_report" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "user_id" integer NOT NULL REFERENCES "udjango_user" ("id")); args=None
2017-10-06 09:56:22[DEBUG]Django.db.backends.schema.execute(): CREATE INDEX "udjango_report_e8701ad4" ON "udjango_report" ("user_id"); (params [])
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) CREATE INDEX "udjango_report_e8701ad4" ON "udjango_report" ("user_id"); args=[]
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) PRAGMA foreign_keys = 0; args=None
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) BEGIN; args=None
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) INSERT INTO "udjango_user" ("email") VALUES ('[email protected]'); args=['[email protected]']
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) BEGIN; args=None
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) INSERT INTO "udjango_user" ("email") VALUES ('[email protected]'); args=['[email protected]']
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) BEGIN; args=None
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) INSERT INTO "udjango_user" ("email") VALUES ('[email protected]'); args=['[email protected]']
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) BEGIN; args=None
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) INSERT INTO "udjango_user" ("email") VALUES ('[email protected]'); args=['[email protected]']
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) BEGIN; args=None
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) INSERT INTO "udjango_report" ("user_id") VALUES (1); args=[1]
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) BEGIN; args=None
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) INSERT INTO "udjango_report" ("user_id") VALUES (3); args=[3]
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) BEGIN; args=None
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) INSERT INTO "udjango_report" ("user_id") VALUES (3); args=[3]
2017-10-06 09:56:22[INFO]root.main(): users without reports
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) SELECT "udjango_user"."id", "udjango_user"."email" FROM "udjango_user" LEFT OUTER JOIN "udjango_report" ON ("udjango_user"."id" = "udjango_report"."user_id") WHERE ("udjango_report"."id" IS NULL AND "udjango_user"."email" LIKE 'a%' ESCAPE '\') LIMIT 21; args=(u'a%',)
2017-10-06 09:56:22[INFO]root.main(): [User(u'[email protected]')]
2017-10-06 09:56:22[INFO]root.main(): users with reports (allows duplicates)
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) SELECT "udjango_user"."id", "udjango_user"."email" FROM "udjango_user" INNER JOIN "udjango_report" ON ("udjango_user"."id" = "udjango_report"."user_id") WHERE ("udjango_report"."id" IS NOT NULL AND "udjango_user"."email" LIKE 'a%' ESCAPE '\') LIMIT 21; args=(u'a%',)
2017-10-06 09:56:22[INFO]root.main(): [User(u'[email protected]'), User(u'[email protected]'), User(u'[email protected]')]
2017-10-06 09:56:22[INFO]root.main(): users with reports (no duplicates)
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) SELECT "udjango_user"."id", "udjango_user"."email" FROM "udjango_user" WHERE (NOT ("udjango_user"."id" IN (SELECT U0."id" AS Col1 FROM "udjango_user" U0 LEFT OUTER JOIN "udjango_report" U1 ON (U0."id" = U1."user_id") WHERE U1."id" IS NULL)) AND "udjango_user"."email" LIKE 'a%' ESCAPE '\') LIMIT 21; args=(u'a%',)
2017-10-06 09:56:22[INFO]root.main(): [User(u'[email protected]'), User(u'[email protected]')]
2017-10-06 09:56:22[INFO]root.main(): users with reports (no duplicates, simpler SQL)
2017-10-06 09:56:22[DEBUG]Django.db.backends.execute(): (0.000) SELECT "udjango_user"."id", "udjango_user"."email" FROM "udjango_user" WHERE ("udjango_user"."email" LIKE 'a%' ESCAPE '\' AND "udjango_user"."id" IN (SELECT U0."user_id" FROM "udjango_report" U0)) LIMIT 21; args=(u'a%',)
2017-10-06 09:56:22[INFO]root.main(): [User(u'[email protected]'), User(u'[email protected]')]
2017-10-06 09:56:22[INFO]root.main(): Done.

Vous pouvez voir que la requête finale utilise toutes les jointures internes.

3
Don Kirkby

Pour filtrer les utilisateurs auxquels aucun rapport n'est associé, essayez ceci:

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

2
Lukasz Koziara