2013-02-12 18 views
32

ho due modelli come questo:In un Django QuerySet, come filtrare per "non esiste" in una relazione molti-a-uno

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

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

In realtà ogni modello ha più campi che sono senza conseguenze a questa domanda.

Desidero filtrare tutti gli utenti che dispongono di un'e-mail che inizia con 'a' e non ha rapporti. Ci saranno più .filter() e .exclude() criteri basati su altri campi.

voglio avvicinarsi in questo modo:

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

users = users.filter(<other filters>) 

users = ??? 

Vorrei ??? per filtrare gli utenti che non hanno rapporti associati con loro. Come lo farei? Se questo non è possibile come l'ho presentato, qual è un approccio alternativo?

risposta

60

Usa isnull.

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

Quando si utilizza isnull=False, il distinct() è necessaria per evitare risultati duplicati.

+6

Questo è OK, ma genera un 'OUTER JOIN' con' report' nel caso di '__isnull = True' e' __isnull = False'. Per la domanda sugli utenti con i report, potrebbe essere meno efficiente di un 'INNER JOIN'. Ho trovato un brutto attacco per esattamente questo caso: 'User.objects.filter (report__id__gt = 0) .distinct()'. Questo presuppone che gli ID siano> 0, il che non deve essere necessariamente un caso. Qualunque modo migliore di forzare un'unione interiore, qualcuno? –

3

Per filtrare gli utenti che non hanno rapporti ad essi associati provare questo:

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

+0

Questo è promettente. Ho bisogno di vedere che tipo di SQL questo genererebbe. Penso anche che 'id__in = Report.objects.all()' sarà sufficiente per la chiamata '.exclude()'. –

+2

Se si utilizzano solo 'users = User.objects.exclude (id__in = Report.objects.all())' si otterranno tutti gli utenti che id è uguale a qualsiasi ID report –

+3

Questa è una cattiva risposta, tranne quando si ' stai solo andando a usare il sito web da solo o all'interno dei tuoi amici più vicini;) Se hai 1 miliardo di rapporti, questo probabilmente causerà il crash del database. Regola generale: non usare mai '__in =' nelle query! Inoltre, non hai nemmeno 'Report.objects.values ​​('user_id').distinct() ', che aiuterebbe un po 'con molti report ma con pochi utenti. –

5

L'unico modo per ottenere SQL nativo ESISTE/NON ESISTE, senza domande in più o unisce è quello di aggiungerlo come SQL crudo in) Clausola (.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) 
]) 

In realtà, si tratta di una abbastanza evidente e soluzione efficiente e talvolta mi chiedo perché non è stato costruito in Django come una ricerca. Inoltre consente di perfezionare la subquery per trovare ad es. solo gli utenti con [fuori] un rapporto durante la la settimana scorsa o con [out] un rapporto senza risposta/non visualizzato.

3

Alasdair's answer è utile, ma non mi piace usare distinct(). A volte può essere utile, ma di solito è un odore di codice che ti dice che hai incasinato i tuoi join.

Fortunatamente, Django queryset consente di filtrare su subquery.

Ecco alcuni modi per eseguire le query dalla tua domanda:

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

Se metti che in un file di Python ed eseguirlo, si dovrebbe vedere qualcosa di simile:

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. 

Puoi vedere che la query finale utilizza tutti i join interni.

+0

Sembra che lo snippet di codice potrebbe non essere completo. L'ultima istruzione è "Report.objects.create (user = anne)" mentre il tuo output e i tuoi pensieri indicano che hai intenzione di mostrare più codice. Sono desideroso di vederlo! –

+0

Non sai di cosa stai parlando, @KrystianCybulski. Il mio browser mostra quel tanto di codice, ma poi c'è una barra di scorrimento per vedere il resto. Se hai problemi con il browser, prova a modificare la risposta per vedere la fonte Markdown. –

+0

Sei corretto. Chiedo scusa. Chrome su Mac è intelligente e nasconde la barra di scorrimento nella casella del codice incorporato. Non avevo capito che è scorrevole. –

Problemi correlati