¿Cómo puedo hacer que las consultas ‘Django ManyToMany’ a través de ‘sean más eficientes?

Estoy usando un ManyToManyField con una clase ‘a través’ y esto genera muchas consultas al buscar una lista de cosas. Me pregunto si hay una manera más eficiente.

Por ejemplo, aquí hay algunas clases simplificadas que describen los libros y sus varios autores, que pasan por una clase de rol (para definir roles como “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) 

Si llamo a Book.authors_names (), entonces puedo obtener una cadena como esta:

John Doe (editor), Fred Bloggs, Billy Bob (ilustrador)

Funciona bien, pero realiza una consulta para obtener los roles del libro y luego otra consulta para cada persona. Si estoy mostrando una lista de libros, esto se traduce en muchas consultas.

¿Hay una manera de hacerlo de manera más eficiente, en una única consulta por libro, con una unión? ¿O es la única forma de usar algo como la selección por lotes ?

(Para los puntos de bonificación … mi encoding de autores_nombres () se ve un poco torpe. ¿Hay alguna forma de hacerlo más elegante al estilo Python?)

Este es un patrón que me encuentro a menudo en Django. Es realmente fácil crear propiedades como su nombre de author_name , y funcionan bien cuando muestra un libro, pero la cantidad de consultas explota cuando desea usar la propiedad para muchos libros en una página.

En primer lugar, puede utilizar select_related para evitar la búsqueda de cada persona

  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) 

Sin embargo, esto no resuelve el problema de buscar los roles para cada libro.

Si está mostrando una lista de libros, puede buscar todos los roles de sus libros en una consulta y luego guardarlos en caché.

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

A continuación, puede acceder a los roles de un libro a través del diccionario roles_by_dict .

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

Tendrá que reconsiderar su propiedad author_name para usar el almacenamiento en caché de esta manera.


También dispararé por los puntos extra.

Agregue un método a rol para representar el nombre completo y el nombre de rol.

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

Los author_names colapsan en una sola línea similar a la sugerencia de Paulo

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

Haría authors = models.ManyToManyField(Role) y tienda nombre completo en Role.alias, porque la misma persona puede firmar libros con distintos seudónimos.

Sobre el torpe, esto:

 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) 

Podría ser:

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