Как сделать Django ManyToMany сквозными запросами более эффективно? - PullRequest
5 голосов
/ 23 ноября 2010

Я использую ManyToManyField со сквозным классом, и это приводит к большому количеству запросов при получении списка вещей.Мне интересно, есть ли более эффективный способ.

Например, вот несколько упрощенных классов, описывающих Книги и их нескольких авторов, которые проходят через класс ролей (для определения ролей, таких как «Редактор», «Иллюстратор»,и т. д.):

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)

Если я позвоню Book.authors_names (), я получу строку примерно так:

Джон Доу (редактор), Фред Блоггс, Билли Боб(Иллюстратор)

Работает нормально, но выполняет один запрос для получения ролей для книги, а затем другой запрос для каждого человека.Если я отображаю список книг, это приводит к большому количеству запросов.

Есть ли способ сделать это более эффективно, в одном запросе на книгу с объединением?Или это единственный способ использовать что-то вроде batch-select ?

(Для бонусных баллов ... мое кодирование author_names () выглядит немного неуклюжим - есть ли способ сделать этоэлегантнее Python-esque?)

Ответы [ 2 ]

8 голосов
/ 23 ноября 2010

Это образец, с которым я часто сталкиваюсь в Джанго. Создать свойства, такие как author_name, очень просто, и они прекрасно работают, когда вы отображаете одну книгу, но количество запросов увеличивается, если вы хотите использовать это свойство для многих книг на странице.

Во-первых, вы можете использовать select_related, чтобы предотвратить поиск для каждого человека

  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)

Однако это не решает проблему поиска ролей для каждой книги.

Если вы отображаете список книг, вы можете просмотреть все роли для своих книг в одном запросе, а затем кэшировать их.

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

Затем вы можете получить доступ к ролям книги через словарь roles_by_dict.

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

Вам придется переосмыслить свойство author_name, чтобы использовать такое кэширование.


Я буду стрелять и по бонусным очкам.

Добавьте метод к роли для отображения полного имени и имени роли.

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

author_names рушится на один лайнер, аналогичный предложению Пауло

@property
def authors_names(self):
   return ', '.join([role.name_and_role for role in self.role_set.all() ])
1 голос
/ 23 ноября 2010

Я бы сделал authors = models.ManyToManyField(Role) и сохранил бы полное имя на Role.alias, потому что один и тот же человек может подписывать книги под разными псевдонимами.

О неуклюжем, это:

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)

Может быть:

def authors_names(self):
   return ', '.join([ '%s (%s)' % (role.person.full_name, role.name) 
                 for role in self.role_set.all() ])
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...