Django 多对多关系通过 table,如何防止它做很多查询?
Django many-to-many relationship with through table, how to prevent it doing a lot of queries?
我正在为 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 以获取活动的所有者,从而避免进行额外的查询。太棒了!
但是,当我还想 return 成员数组(每个成员都有嵌套的用户信息)时,它会执行一个额外的查询来获取成员,然后再执行一个额外的查询 为每个成员 获取用户对象。
我在 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 个查询:一个用于活动(内部与用户 table 所有者),一个用于会员资格,一个用于第一个用户。
prefetch_related
在这里绝对可以提供帮助,但这取决于您如何使用这些关系。
如果您想使用 Campaign
的 members
字段,则:
>>> campaigns = user.campaigns.select_related('owner').prefetch_related('members')
>>> campaigns[0].members.all()[0].name
这将导致两个查询:
- 通过左外连接得到
campaign
得到 owner
- 获取所有相关成员(已经
User
个实例)。
如果你想使用 Campaign
到 Membership
的关系 - 也就是说,membership_set
就像你在序列化程序中使用它的方式:
>>> campaigns = user.campaigns.select_related('owner').prefetch_related('membership_set__user')
>>> campaigns[0].membership_set.all()[0].user.name
这将导致三个查询:
- 通过左外连接得到
campaign
得到 owner
- 获取所有相关的
Membership
个实例
- 从
Membership
的 user
外键中获取所有相关的 Users
(基于 #2 的结果)
编辑:
为了进一步减少第二种方法的查询,您可以使用 Prefetch
指定自定义查询集,您可以使用 select_related
从 [= 中获取 User
20=]:
>>> 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 个查询。
我正在为 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 以获取活动的所有者,从而避免进行额外的查询。太棒了!
但是,当我还想 return 成员数组(每个成员都有嵌套的用户信息)时,它会执行一个额外的查询来获取成员,然后再执行一个额外的查询 为每个成员 获取用户对象。
我在 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 个查询:一个用于活动(内部与用户 table 所有者),一个用于会员资格,一个用于第一个用户。
prefetch_related
在这里绝对可以提供帮助,但这取决于您如何使用这些关系。
如果您想使用 Campaign
的 members
字段,则:
>>> campaigns = user.campaigns.select_related('owner').prefetch_related('members')
>>> campaigns[0].members.all()[0].name
这将导致两个查询:
- 通过左外连接得到
campaign
得到owner
- 获取所有相关成员(已经
User
个实例)。
如果你想使用 Campaign
到 Membership
的关系 - 也就是说,membership_set
就像你在序列化程序中使用它的方式:
>>> campaigns = user.campaigns.select_related('owner').prefetch_related('membership_set__user')
>>> campaigns[0].membership_set.all()[0].user.name
这将导致三个查询:
- 通过左外连接得到
campaign
得到owner
- 获取所有相关的
Membership
个实例 - 从
Membership
的user
外键中获取所有相关的Users
(基于 #2 的结果)
编辑:
为了进一步减少第二种方法的查询,您可以使用 Prefetch
指定自定义查询集,您可以使用 select_related
从 [= 中获取 User
20=]:
>>> 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 个查询。