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

В результате получатся два запроса:

  1. получить campaign с левым внешним объединением для получения owner
  2. получить все связанные члены (уже User экземпляры).

Если вы хотите использовать отношение 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. получить все связанные 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 запросам.

Вернуться на верх