2010-11-23 16 views
5

Ich benutze ein ManyToManyField mit einer 'durch' Klasse und dies führt zu einer Menge von Abfragen beim Abrufen einer Liste von Dingen. Ich frage mich, ob es einen effizienteren Weg gibt.Wie kann ich Django ManyToMany ‚durch‘ Abfragen effizienter machen?

Zum Beispiel sind hier einige vereinfachte Klassen Bücher und ihre verschiedenen Autoren beschreiben, die durch eine Rollenklasse geht (zu definieren Rollen wie „Editor“, „Illustrator“, etc):

class Person(models.Model): 
    first_name = models.CharField(max_length=100) 
    last_name = models.CharField(max_length=100) 

    @property 
    def full_name(self): 
     return ' '.join([self.first_name, self.last_name,]) 

class Role(models.Model): 
    name = models.CharField(max_length=50) 
    person = models.ForeignKey(Person) 
    book = models.ForeignKey(Book) 

class Book(models.Model): 
    title = models.CharField(max_length=255) 
    authors = models.ManyToManyField(Person, through='Role') 

    @property 
    def authors_names(self): 
     names = [] 
     for role in self.role_set.all(): 
      person_name = role.person.full_name 
      if role.name: 
       person_name += ' (%s)' % (role.name,) 
      names.append(person_name) 
     return ', '.join(names) 

Wenn ich rufe Book.authors_names(), dann kann ich einen String etwas wie diese:

John Doe (Herausgeber), Fred Bloggs, Billy Bob (Illustrator)

Es funktioniert gut, aber es gibt eine Abfrage, um die Rollen für das Buch zu erhalten, und dann eine weitere Abfrage für jede Person. Wenn ich eine Liste von Büchern anzeigen lasse, ergeben sich viele Anfragen.

Gibt es eine Möglichkeit, dies effizienter zu tun, in einer einzigen Abfrage pro Buch, mit einem Join? Oder ist der einzige Weg, etwas wie batch-select zu verwenden?

(für Bonuspunkte ... meine Codierung von authors_names() sieht ein bisschen klobig - ist es eine Möglichkeit, es elegante Python-esque zu machen?)

+2

‚Python-artige‘ in der Regel für Vergleiche zu Monty Python vorbehalten: das Wort für Sie suchen ist ‚Pythonic‘. –

+1

@daniel: +1 für die korrekte Verwendung von 'Pythonic', obwohl die Verwendung von 'Python-esque' bedeuten kann, dass der Autor versucht, den Code ein bisschen lustiger zu machen ... –

+3

Danke für die Korrektur. Ich werde mich jedoch von nun an bemühen, meinen Code nicht nur korrekter, sondern auch lustiger zu machen. –

Antwort

8

Dies ist ein Muster, das ich in Django oft begegnet. Es ist wirklich einfach Eigenschaften wie Ihre author_name zu schaffen, und sie arbeiten groß, wenn Sie ein Buch anzuzeigen, aber die Anzahl der Abfragen explodiert, wenn Sie die Eigenschaft für viele Bücher auf einer Seite verwenden möchten.

Zum einen können Sie select_related verwenden, um die Suche für jede Person

for role in self.role_set.all().select_related(depth=1): 
     person_name = role.person.full_name 
     if role.name: 
      person_name += ' (%s)' % (role.name,) 
     names.append(person_name) 
    return ', '.join(names) 

jedoch zu verhindern, wird das Problem nicht lösen, für jedes Buch die Rollen nach oben.

Wenn Sie eine Liste von Büchern anzeigen, können Sie alle Rollen für Ihre Bücher in einer Abfrage nachschlagen und anschließend zwischenspeichern.

>>> books = Book.objects.filter(**your_kwargs) 
>>> roles = Role.objects.filter(book_in=books).select_related(depth=1) 
>>> roles_by_book = defaultdict(list) 
>>> for role in roles: 
... roles_by_book[role.book].append(books)  

können Sie dann die Rollen eines Buches Zugriff durch das roles_by_dict Wörterbuch.

>>> for book in books: 
... book_roles = roles_by_book[book] 

Sie müssen Ihre author_name Eigenschaft zu überdenken Caching wie diese zu verwenden.


Ich werde für die Bonuspunkte auch schießen.

Fügen Sie der Rolle eine Methode hinzu, um den vollständigen Namen und den Rollennamen zu rendern.

class Role(models.Model): 
    ... 
    @property 
    def name_and_role(self): 
     out = self.person.full_name 
     if self.name: 
      out += ' (%s)' % role.name 
     return out 

Die author_names kollabiert zu einem Einzeiler ähnlich wie Paulo Vorschlag

@property 
def authors_names(self): 
    return ', '.join([role.name_and_role for role in self.role_set.all() ]) 
+0

Ah, vielen Dank für den select_related() Zeiger Alasdair, das ist eine Verbesserung. Ich muss darüber nachdenken, wie Sie den zweiten Teil Ihrer Antwort am besten in meinem Code verwenden. Vielleicht in einem benutzerdefinierten Manager? –

+0

Ich war noch nicht auf Batch-Auswahl, aber es sieht vielversprechend aus. Ich könnte das untersuchen, bevor ich versuche, meinen eigenen benutzerdefinierten Manager zu schreiben. – Alasdair

+0

Rolle .__ Unicode__ ist ein idealer Kandidat für das Rendern –

1

I authors = models.ManyToManyField(Role) und speichert Fullnamen in Rollen würde. Alias, weil dieselbe Person Bücher unter verschiedenen Pseudonymen signieren kann.

Über den klobigen, dies:

def authors_names(self): 
    names = [] 
    for role in self.role_set.all(): 
     person_name = role.person.full_name 
     if role.name: 
      person_name += ' (%s)' % (role.name,) 
     names.append(person_name) 
    return ', '.join(names) 

Könnte sein:

def authors_names(self): 
    return ', '.join([ '%s (%s)' % (role.person.full_name, role.name) 
       for role in self.role_set.all() ]) 
+0

Um ehrlich zu sein, mache ich mir keine Sorgen um Pseudonyme - für die Zwecke dieses Projekts ist der Name eines Autors, pseudonym oder nicht, die Person. –

+0

Und danke für den Code Vorschlag. Aber das ist nicht dasselbe - wenn es keine Rolle gibt. Name Ich möchte keine leeren Klammern hinter dem Namen des Autors. –

Verwandte Themen