如何提升Django ManyToMany 'through' 查询效率?
我正在使用一个叫做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 个回答
我会把 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() ])
在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() ])