如何提升Django ManyToMany 'through' 查询效率?

6 投票
2 回答
3346 浏览
提问于 2025-04-16 07:29

我正在使用一个叫做ManyToManyField的功能,并且用了一个“through”类,这样在获取一系列东西的时候会产生很多查询。我在想有没有更高效的方法。

举个例子,这里有一些简化的类,用来描述书籍和它们的多个作者,这些作者通过一个角色类(Role)来定义,比如“编辑”、“插画师”等等:

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(),我可以得到类似这样的字符串:

约翰·多(编辑),弗雷德·布洛格斯,比利·鲍勃(插画师)

这个方法运行得很好,但它需要先查询书籍的角色,然后对每一个人再进行一次查询。如果我在显示一本书的列表,这样就会产生很多查询。

有没有办法更高效一点,让每本书只用一次查询,能用到连接(join)?或者唯一的办法就是使用像batch-select这样的工具吗?

(额外加分... 我写的authors_names()看起来有点笨重,有没有办法让它更优雅一些,更像Python的风格?)

2 个回答

1

我会把 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() ])
8

在Django中,我经常遇到这样一个模式。创建像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就可以简化成一行,类似于Paulo的建议。

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

撰写回答