首页 > 解决方案 > Django 与 through 表的多对多关系,如何防止它进行大量查询?

问题描述

我正在为 D&D 工具开发 API,我在其中有活动,人们可以成为活动的成员。我需要为活动的每个成员存储额外的信息,因此我使用的是直通模型。

class Campaign(models.Model):
    name = models.CharField(max_length=50)
    description = models.TextField()
    owner = models.ForeignKey(User, related_name='owned_campaigns', on_delete=models.CASCADE)
    members = models.ManyToManyField(User, related_name='campaigns', through='Membership')


class Membership(models.Model):
    campaign = models.ForeignKey(Campaign, on_delete=models.CASCADE)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    is_dm = models.BooleanField(default=False)

如果我想获取用户所属的所有广告系列,我可以简单地执行以下操作:

>>> from auth.models import User
>>> user = User.objects.get(pk=1)
>>> campaigns = user.campaigns.select_related('owner')
>>> print(campaign)

这会执行 INNER JOIN 来获取活动的所有者,从而避免进行额外的查询。伟大的!

但是,当我还想返回成员数组(每个成员的嵌套用户信息)时,它会执行一个额外的查询来获取成员,然后为每个成员执行一个额外的查询来获取用户对象。

我在 Django REST Framework 中特别注意到了这一点,其中我有这样的序列化程序:

class MembershipSerializer(serializers.ModelSerializer):
    user = UserSerializer()

    class Meta:
        model = Membership
        fields = ["is_dm", "user"]


class CampaignSerializer(serializers.ModelSerializer):
    owner = UserSerializer()
    members = MembershipSerializer(many=True, read_only=True, source='membership_set')

    class Meta:
        model = Campaign
        fields = '__all__'

如果你有一个有 6 个成员的活动,那么这总共会产生 8 个查询,这对我来说似乎很愚蠢。

我曾希望这prefetch_related能解决这个问题:

>>> campaigns = user.campaigns.select_related('owner').prefetch_related()
>>> campaigns[0].members.all()[0].name

但这仍然执行 3 个查询:一个用于活动(与所有者的用户表内部连接),一个用于成员资格,一个用于第一个用户。

标签: pythondjangodjango-modelsdjango-queryset

解决方案


prefetch_related在这里绝对可以提供帮助,但这取决于您如何使用这些关系。

如果要使用Campaign'members字段,则:

>>> campaigns = user.campaigns.select_related('owner').prefetch_related('members')
>>> campaigns[0].members.all()[0].name

这将导致两个查询:

  1. campaign使用左外连接获取owner
  2. 获取所有相关成员(已经是User实例)。

如果您想使用 to 的关系Campaign-Membership也就是说,membership_set就像您在序列化程序中使用它的方式一样:

>>> campaigns = user.campaigns.select_related('owner').prefetch_related('membership_set__user')
>>> campaigns[0].membership_set.all()[0].user.name

这将导致三个查询:

  1. campaign使用左外连接获取owner
  2. 获取所有相关Membership实例
  3. 从(基于#2的结果)Usersuser外键获取所有相关信息Membership

编辑

为了进一步减少第二种方法的查询,您可以使用Prefetch指定自定义查询集,您可以在其中select_related获取Userfrom Membership

>>> campaigns = user.campaigns.select_related('owner').prefetch_related(Prefetch('membership_set', queryset=Membership.objects.all().select_related('user')))
>>> campaigns[0].membership_set.all()[0].user.name

这将对Membership和相关的进行连接Users,然后最终应该只产生 2 个查询。


推荐阅读