Django отношения "многие-ко-многим" со сквозной таблицей, как предотвратить выполнение большого количества запросов?
Я работаю над API для инструмента D&D, где у меня есть кампании, и люди могут быть членами кампаний. Мне нужно хранить дополнительную информацию для каждого члена кампании, поэтому я использую сквозную модель.
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 запроса: один для кампании (с внутренним объединением с таблицей пользователей для владельца), один для членства и один для того первого пользователя.
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
экземпляры - получить все связанные
Users
изuser
внешнего ключаMembership
(основываясь на результатах #2)
EDIT:
Чтобы еще больше сократить количество запросов для второго подхода, вы можете использовать Prefetch
для указания пользовательского набора запросов, в котором вы можете использовать select_related
для получения User
из 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 запросам.