2017-06-13 6 views
1

Die Modelle

Hier ist die Grundmodelle Setup haben wir.Filterung Django QuerySet basierend auf ManyToMany Beziehung Existenz

Eine Liste enthält viele Elemente, und ein Element kann in vielen Listen enthalten sein. Für einen gegebenen Artikel, wenn irgendeiner seiner Listen gut ist (d. H. list.bad == False), dann ist der Artikel gut. Wenn ein Artikel in keiner guten Listen erscheint, dann ist es schlecht.

Wir haben ein benutzerdefiniertes QuerySet für Items, mit einer Methode, nur gute Items zurückzugeben, und einer Methode, nur fehlerhafte Items zurückzugeben.

class Item(models.Model): 
    objects = ItemQuerySet.as_manager() 
    name = models.CharField(max_length=255, unique=True) 

class List(models.Model): 
    name = models.CharField(max_length=255, unique=True) 
    bad = models.BooleanField(default=True) 
    items = models.ManyToManyField(Item, related_name='lists') 

class ItemQuerySet(models.QuerySet): 
    def bad(self): 
     return self.exclude(lists__bad=False) 

    def good(self): 
     return self.filter(lists__bad=False) 

Das Szenario

Hier ist ein Beispiel für ein Szenario, das wir Probleme haben mit: einer schlechten List, eine gute Liste, und zwei Artikel.

BadList: GoodList: 
- Item1  - Item1 
- Item2 

Da element1 in mindestens eine gute Liste erscheint, sollte es kommen in Item.objects.good(), und nicht in Item.objects.bad().

Da Item2 in keiner guten Liste erscheint, sollte es in Item.objects.bad() erscheinen und nicht in Item.objects.good().

Wir können das Szenario wie so ein:

# Create the two lists. 
>>> goodlist = List.objects.create(name='goodlist', bad=False) 
>>> badlist = List.objects.create(name='badlist', bad=True) 

# Create the two items. 
>>> item1 = Item.objects.create(name='item1') 
>>> item2 = Item.objects.create(name='item2') 

# Item1 goes in both lists 
>>> goodlist.items.add(item1) 
>>> badlist.items.add(item1) 

# Item2 only in badlist 
>>> badlist.items.add(item2) 

Und in der Tat, Item.objects.good() und Item.objects.bad() Arbeit, wie wir erwarten:

>>> Item.objects.bad() # This returns what we want! Good! 
<QuerySet [<Item: item2>]> 

>>> Item.objects.good() # This returns what we want! Good! 
<QuerySet [<Item: item1>]> 

Das Problem

Dank für das mit mir tragen . Hier ist unser benutzerdefiniertes QuerySet falsch. Wenn wir auf die good() und bad() benutzerdefinierten QuerySet-Methoden bis Elemente einer einzelnen Liste zugreifen, erhalten wir falsche Ergebnisse.

>>> badlist.items.bad() # WRONG! We want to ONLY see item2 here! 
<QuerySet [<Item: item1>, <Item: item2>] 

>>> badlist.items.good() # WRONG! We want to see item1 here! 
<QuerySet []> 

Es scheint so, als wir badlist.items.bad() tun, um die Abfrage nur hält badlist bei der Bestimmung, ob die Gegenstände schlecht sind, anstatt alle Listen bedenkt, dass die Einzelteile sind in. Aber ich bin verwirrt, warum das wäre der Fall.

Mein Gedanke ist, dass in der ItemQuerySet.bad Methode, ich möchte etwas wie self.exclude(any__lists__bad=False) statt nur self.exclude(lists__bad=False). Aber natürlich existiert das Schlüsselwort any__ nicht wirklich, und ich bin nicht sicher, wie ich diese Logik in einem Django QuerySet korrekt ausdrücken kann. Es scheint, dass die Verwendung Q Objekte der Weg nach vorne sein könnte, aber ich bin immer noch nicht ganz sicher, wie eine Abfrage wie folgt mit Q Objekte auszudrücken.

In unserer tatsächlichen Datenbank gibt es weniger als 100 Listen, aber Millionen von Elementen. Aus Performance-Gründen ist es daher ideal, dies mit einer Abfrage zu tun und nicht mit einer Eigenschaft oder mehreren Abfragen.

Prost!

Antwort

1

Wenn Sie die von generierte Abfrage ausdrucken, sehen Sie das Problem: Es wird eine WHERE-Klausel für die Durchgangs-Tabelle verwendet, wodurch die Listen auf nur Badlist beschränkt werden. Sie müssen von der Item Ebene starten, wenn Sie bad und good korrekt anwenden möchten, dann nach den Elementen in der Liste filtern.

item_ids = list(badlist.items.values_list('id'), flat=True) 

Item.objects.bad().filter(id__in=item_ids) 

Item.objects.good().filter(id__in=item_ids) 

Edit: Ich kann das nicht testen, ohne das Schema, aber ich denke, Sie Anmerkungen verwenden können, um die Anzahl der Listen zu zählen und filtern dann über diese

def annotate_good(self); 
    return self.annotate(good=Count(Case(When(lists__bad=False, then=1), default=0))) 

def good(self): 
    return self.annotate_good().exclude(good=0) 

def bad(self): 
    return self.annotate_good().filter(good=0) 

Andernfalls, wenn die Leistung wirklich ist ein Problem, ich würde dem Item-Modell ein gutes oder ein schlechtes Feld hinzufügen und es beim Speichern aktualisieren, so dass die Abfrage ganz einfach wird.

+0

Danke für die Antwort! Das funktioniert zwar richtig, aber ich bin besorgt, dass andere Leute, die mit dem Code arbeiten, versuchen werden, die 'badlist.items.good()' Route zu benutzen, ohne es besser zu wissen. Ich möchte vermeiden, dass Menschen so in die Irre geführt werden. Darüber hinaus wollte ich dies im ursprünglichen Beitrag erwähnen: In unserer tatsächlichen Datenbank gibt es weniger als 100 Listen, aber Millionen von Items. Aus Performance-Gründen ist es daher ideal, dies mit einer Abfrage zu tun und nicht mit einer Eigenschaft oder mehreren Abfragen. –

+1

Ah, hab's. Ich denke, Sie können dies mit der Anfrageset-Annotation tun. Ich habe meine Antwort oben mit einer Lösung bearbeitet, die meiner Meinung nach funktionieren sollte. Andernfalls würde ich zur Verbesserung der Leistung einfach die schlecht/gut Spalte zu Item hinzufügen und diese aktualisieren, was die Abfrage viel einfacher macht – Brobin

Verwandte Themen