2012-09-17 16 views
12

Angenommen, ich habe ein Modell Box mit einer GenericForeignKey, die entweder auf eine Apple Instanz oder eine Chocolate Instanz zeigt. Apple und Chocolate wiederum haben ForeignKeys zu Farm bzw. Factory. Ich möchte eine Liste von Box es anzeigen, für die ich auf Farm und Factory zugreifen muss. Wie mache ich das in so wenigen DB-Abfragen wie möglich?django: Prefetch verwandte Objekte eines GenericForeignKey

Minimal anschauliches Beispiel:

class Farm(Model): 
    ... 

class Apple(Model): 
    farm = ForeignKey(Farm) 
    ... 

class Factory(Model): 
    ... 

class Chocolate(Model): 
    factory = ForeignKey(Factory) 
    ... 

class Box(Model) 
    content_type = ForeignKey(ContentType) 
    object_id = PositiveIntegerField() 
    content_object = GenericForeignKey('content_type', 'object_id') 
    ... 

    def __unicode__(self): 
     if self.content_type == ContentType.objects.get_for_model(Apple): 
      apple = self.content_object 
      return "Apple {} from Farm {}".format(apple, apple.farm) 
     elif self.content_type == ContentType.objects.get_for_model(Chocolate): 
      chocolate = self.content_object 
      return "Chocolate {} from Factory {}".format(chocolate, chocolate.factory) 

Hier sind ein paar Dinge, die ich versuchte. In all diesen Beispielen ist N die Anzahl der Boxen. Der Abfrage-Count geht davon aus, dass die ContentType s für Apple und Chocolate bereits zwischengespeichert wurden, so dass die get_for_model() Anrufe nicht die DB treffen.

1) Naive:

print [box for box in Box.objects.all()]

Dies gilt (Fetch Boxes) + N (Fetch Apple oder Schokolade für jede Box) + N (Fetch Farm für jeden Apple und Factory für jede Schokolade) Abfragen.

2) select_related hilft hier nicht, denn Box.content_object ist ein GenericForeignKey.

3) Ab django 1.4, prefetch_related kann GenericForeignKey s holen.

print [box for box in Box.objects.prefetch_related('content_object').all()]

Dies gilt (Fetch Boxes) + (holen Äpfel und Schokolade für alle Boxen) + N (Fetch Farm für jeden Apple und Fabrik für jede Schokolade) Abfragen.

4) Anscheinend prefetch_related ist nicht intelligent genug, ForeignKeys von GenericForeignKeys zu folgen. Wenn ich versuche:

print [box for box in Box.objects.prefetch_related( 'content_object__farm', 'content_object__factory').all()]

es beschwert sich zu Recht, dass Chocolate Objekte haben keine farm Feld, und umgekehrt.

5) Ich konnte tun:

apple_ctype = ContentType.objects.get_for_model(Apple) 
chocolate_ctype = ContentType.objects.get_for_model(Chocolate) 
boxes_with_apples = Box.objects.filter(content_type=apple_ctype).prefetch_related('content_object__farm') 
boxes_with_chocolates = Box.objects.filter(content_type=chocolate_ctype).prefetch_related('content_object__factory') 

Dies gilt (Fetch Boxes) + (holen Äpfel und Schokolade für alle Boxen) + (Fetch Farms für alle Äpfel und Fabriken für alle Schokoladen) Abfragen. Der Nachteil ist, dass ich die beiden Abfragesätze (boxes_with_apples, boxes_with_chocolates) manuell zusammenführen und sortieren muss. In meiner realen Anwendung zeige ich diese Boxen in einem paginierten ModelAdmin an. Es ist nicht offensichtlich, wie diese Lösung dort integriert werden kann. Vielleicht könnte ich einen benutzerdefinierten Paginator schreiben, um diesen Cache transparent zu machen?

6) Ich könnte etwas zusammen bauen, basierend auf this, das auch O (1) Abfragen durchführt. Aber ich würde lieber nicht mit Interna (_content_object_cache) Probleme machen, wenn ich es vermeiden kann.

Zusammengefasst: Drucken einer Box erfordert Zugriff auf die ForeignKeys eines GenericForeignKey. Wie kann ich N Boxen in O (1) Abfragen drucken? Ist (5) das Beste, was ich tun kann, oder gibt es eine einfachere Lösung?

Bonuspunkte: Wie würden Sie dieses DB-Schema umgestalten, um solche Abfragen zu vereinfachen?

+0

verwenden Wenn Sie umbenennen 'Zuchtbetrieb /' factory' zu einem gemeinsamen Namen, wie 'creator', Arbeit prefetch_related wird? – Igor

+0

Tatsächlich funktioniert 'prefetch_related ('content_object__creator')' nach dem vorgeschlagenen Umbenennen. Leider kann das Umbenennen in Abhängigkeit von den tatsächlichen Modellen, die Sie anstelle von Apple/Farm und Chocolate/Factory haben, sinnvoll sein oder auch nicht. – cberzan

Antwort

8

Sie können etwas wie prefetch_selected manuell implementieren und Djangos select_related-Methode verwenden, die Datenbankabfrage beitreten wird.

apple_ctype = ContentType.objects.get_for_model(Apple) 
chocolate_ctype = ContentType.objects.get_for_model(Chocolate) 
boxes = Box.objects.all() 
content_objects = {} 
# apples 
content_objects[apple_ctype.id] = Apple.objects.select_related(
    'farm').in_bulk(
     [b.object_id for b in boxes if b.content_type == apple_ctype] 
    ) 
# chocolates 
content_objects[chocolate_ctype.id] = Chocolate.objects.select_related(
    'factory').in_bulk(
     [b.object_id for b in boxes if b.content_type == chocolate_ctype] 
    ) 

Dies sollte nur 3 Abfragen (get_for_model Abfragen werden weggelassen) machen. Die Methode in_bulk gibt Ihnen ein Diktat im Format {id: model} zurück. So bekommen Sie Ihre content_object benötigen Sie einen Code wie:

content_obj = content_objects[box.content_type_id][box.object_id] 

aber ich bin nicht sicher, ob dieser Code wird schneller dann O (5) Lösung, da es zusätzliche Iteration über Boxen erfordert QuerySet und auch sie generiert Abfrage mit WHERE id IN (...) Anweisung

Aber wenn Sie Felder nur nach Feldern von Box-Modell sortieren, können Sie die content_objects dict nach Seitenumbruch füllen. Aber Sie müssen content_objects zu __unicode__ irgendwie

passieren Wie würden Sie dieses DB-Schema Refactoring solche Abfragen zu erleichtern?

Wir haben eine ähnliche Struktur. Wir speichern content_object in Box, aber statt object_id und content_object verwenden wir ForeignKey(Box) in Apple und Chocolate. In Box haben wir eine get_object Methode, Apple oder Chocolate Modell zurückzugeben. In diesem Fall können wir select_related verwenden, aber in den meisten unserer Anwendungsfälle filtern wir Boxen nach content_type. Also haben wir die gleichen Probleme wie Ihre fünfte Option. Aber wir starteten das Projekt auf Django 1.2, wenn prefetch_selected nicht ausgewählt war.

Wenn Sie farm/factory in einen gebräuchlichen Namen umbenennen, wie creator, funktioniert prefetch_related?

über Ihre Option 6

kann ich nichts sagen gegen _content_object_cache füllen. Wenn Sie mit Einbauten nicht beschäftigen möchten, können Sie benutzerdefinierte Eigenschaft füllen und dann

apple = getattr(self, 'my_custop_prop', None) 
if apple is None: 
    apple = self.content_object 
+0

Habe gerade gemerkt, dass meine Antwort sehr nah an deiner * Option 6 * liegt, aber mit weniger Automatisierung. Ich habe diesen Artikel nie zuvor gelesen.Auch das sieht nicht wie O (1) aus, es ist eher O (2 + number_of_unique_ctypes) – Igor