2013-02-12 6 views
32

mam dwa modele tak:W Django QuerySet, jak filtrować za „nie istnieje” w relacji wiele-do-jednego

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

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

W rzeczywistości każdy model ma więcej pól, które są bez znaczenia na to pytanie.

Chcę filtrować wszystkich użytkowników, którzy mają e-mail, który zaczyna się od "a" i nie ma raportów. Będzie więcej kryteriów opartych na innych polach.

Chcę podejść do niego tak:

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

users = users.filter(<other filters>) 

users = ??? 

Chciałbym ??? odfiltrować użytkowników, którzy nie mają powiązanych z nimi raportów. Jak to zrobić? Jeśli to nie jest możliwe, tak jak je przedstawiłem, jakie jest alternatywne podejście?

Odpowiedz

60

Korzystając isnull.

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

Podczas korzystania isnull=False The distinct() jest wymagane, aby zapobiec duplikaty wyników.

+6

To jest OK, ale generuje 'ZEWNĘTRZNE JOINNE' z' raportem 'w przypadku zarówno '__isnull = True' i' __isnull = False'. W przypadku pytania o użytkowników z raportami może być ono mniej efektywne niż "INNER JOIN". Znalazłem brzydki hack w tym przypadku: 'User.objects.filter (report__id__gt = 0) .distinct()'. Zakłada się, że identyfikatory są> 0, co nie musi być przypadkiem. Czy jest jakiś lepszy sposób na wymuszenie wewnętrznego sprzężenia? –

3

Aby odfiltrować użytkowników, którzy nie mają raporty z nimi spróbuj tego:

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

+0

To obiecujące. Muszę zobaczyć, jaki rodzaj SQL to wygeneruje. Myślę też, że 'id__in = Report.objects.all()' wystarczy dla wywołania '.exclude()'. –

+2

Jeśli użyjesz tylko 'users = User.objects.exclude (id__in = Report.objects.all())' otrzymasz wszystkich użytkowników, których identyfikator jest taki sam jak dowolny identyfikator raportu –

+3

To jest zła odpowiedź, z wyjątkiem sytuacji, gdy " zamierzasz używać samej witryny lub swoich bliskich znajomych;) Jeśli masz miliard raportów, prawdopodobnie spowoduje to awarię bazy danych. Zasada: nigdy nie używaj '__in =' w zapytaniach! Poza tym, nawet nie masz 'Report.objects.values ​​('user_id').distinct() ', które pomogłyby w wielu raportach, ale w niewielu scenariuszach użytkowników. –

5

Jedynym sposobem, aby dostać się natywną SQL EXISTS/NOT EXISTS bez dodatkowych zapytań lub dołączający jest dodanie go jako surowego SQL w .extra() klauzuli:

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

W rzeczywistości, jest to dość oczywiste i wydajne rozwiązanie i czasami zastanawiam się, dlaczego nie został on wbudowany w Django jako odnośnik. Pozwala także na sprecyzowanie podzapytania w celu znalezienia np. tylko użytkownicy z [out] raportem podczas w zeszłym tygodniu lub z [out] raportem bez odpowiedzi/bez podglądu.

3

Alasdair's answer jest pomocna, ale nie podoba mi się użycie distinct(). Czasami może się przydać, ale zwykle jest to zapach kodu, który mówi, że zawiodłeś swoje połączenia.

Na szczęście narzędzie Django pozwala na filtrowanie podzapytań.

Oto kilka sposobów, aby uruchomić zapytania z pytaniem:

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

Jeśli umieścisz że do pliku Python i uruchomić go, powinieneś zobaczyć coś takiego:

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. 

Możesz zobaczyć, że zapytanie końcowe wykorzystuje wszystkie połączenia wewnętrzne.

+0

Wygląda na to, że fragment kodu może być niepełny. Ostatnie stwierdzenie to 'Report.objects.create (user = anne)' podczas gdy twoje wyniki i przemyślenia wskazują, że chciałeś pokazać więcej kodu. Bardzo chcę to zobaczyć! –

+0

Nie jestem pewien, o czym mówisz, @KrystianCybulski. Moja przeglądarka pokazuje o tym dużo kodu, ale jest też pasek przewijania, aby zobaczyć resztę. Jeśli masz problemy z przeglądarką, spróbuj edytować odpowiedź, aby zobaczyć źródło oznaczenia. –

+0

Masz rację. Przepraszam. Chrome na Macu jest sprytny i ukrywa pasek przewijania w osadzonym polu kodu. Nie zdawałem sobie sprawy, że można je przewijać. –

Powiązane problemy