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

6
我正在使用带有“through”类的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(),那么我可以得到一个类似于以下内容的字符串:
John Doe(编辑),Fred Bloggs,Billy Bob(插图)
它运行良好,但它会执行一次查询以获取书籍的角色,然后为每个人执行另一个查询。如果我正在显示书籍列表,则这将累积到大量查询。
有没有更有效率的方法,在单个查询中使用join每本书进行操作?或者使用batch-select之类的东西是唯一的方法?
(对于额外的奖励点...我的编写作者的方式看起来有些笨拙 - 是否有一种更优雅的Python-esque方式?)

2
“Python-esque”通常用于与蒙提·派森(Monty Python)的比较,你要找的词是“Pythonic”。 - Daniel Roseman
1
@daniel:对于“pythonic”的正确用法给你点赞,尽管使用“python-esque”可能意味着作者想让代码变得更有趣一些... - Bernhard Vallant
3
谢谢您的纠正。从现在开始,我将努力让我的代码不仅更加正确,而且更有趣。 - Phil Gyford
2个回答

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
< p > author_names 折叠成一行,类似于 Paulo 的建议

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

啊,非常感谢Alasdair提供的select_related()指针,这是一个进步。我需要考虑如何最好地在我的代码中使用你回答的第二部分。也许可以在自定义管理器中实现? - Phil Gyford
我之前还没有接触过批量选择,但它看起来很有前景。在尝试编写自定义管理器之前,我可能会先调查一下这个。 - Alasdair

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

说实话,我不担心笔名 - 对于这个项目的目的来说,作者的姓名,无论是真名还是笔名,都代表着这个人。 - Phil Gyford
谢谢您的代码建议。但是那并不完全是同样的事情 - 如果没有 role.name,我不希望在作者名字后面有空括号。 - Phil Gyford

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接