2010-11-23 5 views
5

J'utilise un ManyToManyField avec une classe 'through', ce qui entraîne de nombreuses requêtes lors de l'extraction d'une liste de choses. Je me demande s'il y a un moyen plus efficace.Comment rendre les requêtes Django ManyToMany 'through' plus efficaces?

Par exemple voici quelques classes simplifiées décrivant livres et leurs nombreux auteurs, qui passe par une classe de rôle (pour définir les rôles comme « éditeur », « 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) 

Si je l'appelle Book.authors_names() alors je peux obtenir quelque chose comme une chaîne ceci:

John Doe (Sous la direction de), Fred Bloggs, Billy Bob (Illustrations)

Cela fonctionne bien, mais il fait une requête pour obtenir les rôles pour le livre, puis une autre requête pour chaque personne. Si j'affiche une liste de livres, cela ajoute beaucoup de questions.

Existe-t-il un moyen de le faire plus efficacement, dans une seule requête par livre, avec une jointure? Ou est le seul moyen d'utiliser quelque chose comme batch-select?

(Pour les points bonus ... mon codage de authors_names() semble un peu maladroit - est-il un moyen de le rendre plus élégant Python-esque?)

+2

Python-esque »est habituellement réservé aux comparaisons de Monty Python: le mot que vous cherchez est 'Pythonic'. –

+1

@daniel: +1 pour l'utilisation correcte de 'pythonic', bien que l'utilisation de 'python-esque' puisse impliquer que l'auteur cherche à rendre le code un peu plus drôle ... –

+3

Merci pour la correction. Cependant, je m'efforcerai désormais de rendre mon code non seulement plus correct mais aussi plus drôle. –

Répondre

8

C'est un modèle que je viens souvent à travers Django. Il est vraiment facile de créer des propriétés telles que votre author_name, et elles fonctionnent très bien lorsque vous affichez un livre, mais le nombre de requêtes explose lorsque vous souhaitez utiliser la propriété pour de nombreux livres sur une page.

Tout d'abord, vous pouvez utiliser select_related pour empêcher la recherche pour toute personne

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) 

Cependant, cela ne résout pas le problème de la recherche les rôles pour chaque livre.

Si vous affichez une liste de livres, vous pouvez rechercher tous les rôles de vos livres dans une requête, puis les mettre en cache.

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

Vous pouvez alors accéder aux rôles d'un livre dans le dictionnaire roles_by_dict.

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

Vous devrez repenser votre propriété author_name d'utiliser la mise en cache comme celui-ci.


Je vais aussi tirer pour les points bonus.

Ajoutez une méthode au rôle pour afficher le nom complet et le nom du rôle.

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

Les author_names effondrements à une seule ligne similaire à la suggestion de Paulo

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

Ah, merci beaucoup pour le pointeur select_related() Alasdair, c'est une amélioration. Je vais devoir réfléchir à la meilleure façon d'utiliser la deuxième partie de votre réponse dans mon code. Peut-être dans un gestionnaire personnalisé? –

+0

Je n'avais pas rencontré de sélection par lots auparavant, mais cela semble prometteur. Je pourrais étudier cela avant d'essayer d'écrire mon propre gestionnaire de coutume. – Alasdair

+0

Rôle .__ unicode__ est un candidat idéal pour le rendu –

1

je faire authors = models.ManyToManyField(Role) et stocker fullname au rôle. alias, parce que la même personne peut signer des livres sous des pseudonymes distincts.

A propos du clunky, ceci:

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) 

Peut-être:

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

Je ne m'inquiète pas pour les pseudonymes pour être honnête - aux fins de ce projet, le nom d'un auteur, pseudonyme ou non, est la personne. –

+0

Et merci pour la suggestion de code. Mais cela ne fait pas exactement la même chose - s'il n'y a pas de nom de rôle. Je ne veux pas de parenthèses vides après le nom de l'auteur. –

Questions connexes