Как мне собрать все данные Django из базы данных перед вызовом моего сериализатора?

Я использую Python 3.9 и Django 3.2. У меня есть модель Django с парой отношений "многие-ко-многим"

class Coop(models.Model):
    objects = CoopManager()
    name = models.CharField(max_length=250, null=False)
    types = models.ManyToManyField(CoopType, blank=False)
    addresses = models.ManyToManyField(Address, through='CoopAddressTags')
    enabled = models.BooleanField(default=True, null=False)
    phone = models.ForeignKey(ContactMethod, on_delete=models.CASCADE, null=True, related_name='contact_phone')
    email = models.ForeignKey(ContactMethod, on_delete=models.CASCADE, null=True, related_name='contact_email')
    web_site = models.TextField()
    description = models.TextField(null=True)
    approved = models.BooleanField(default=False, null=True)
    proposed_changes = models.JSONField("Proposed Changes", null=True)
    reject_reason = models.TextField(null=True)

Я могу искать свою модель, используя класс менеджера, который строит запрос следующим образом...

    def find(
        self,
        partial_name,
        types_arr=None,
        enabled=None,
        city=None,
        zip=None,
        street=None,
        state_abbrev=None
    ):
        """
        Lookup coops by varying criteria.
        """
        q = Q()
        if partial_name:
            q &= Q(name__icontains=partial_name)
        if enabled != None:
            q &= Q(enabled=enabled)
        if types_arr != None:
            filter = Q(
                *[('types__name', type) for type in types_arr],
                _connector=Q.OR
            )
            q &= filter
        if street != None:
            q &= Q(addresses__raw__icontains=street)
        if city != None:
            q &= Q(addresses__locality__name__iexact=city)
        if zip != None:
            q &= Q(addresses__locality__postal_code=zip)
        if state_abbrev != None:
            q &= Q(addresses__locality__state__code=state_abbrev)
            q &= Q(addresses__locality__state__country__code="US")

        queryset = Coop.objects.filter(q)
        print(queryset.query)
        return queryset

В моем представлении я вызываю и возвращаю данные с помощью

coops = Coop.objects.find(...)
serializer = CoopSearchSerializer(coops, many=True)

в котором сериализатор выглядит как

class CoopSearchSerializer(serializers.ModelSerializer):
    ...
    def to_representation(self, instance):
        rep = super().to_representation(instance)
        rep['coopaddresstags_set'] = CoopAddressTagsSerializer(instance.coopaddresstags_set.all(), many=True).data
        return rep

Я заметил, что когда я ищу набор результатов, содержащий 6 результатов, и каждый из них сериализуется с использованием вышеприведенной логики, я получаю пропорциональное количество запросов, выполняемых для каждого результата ...

 SELECT "directory_coopaddresstags"."id", "directory_coopaddresstags"."coop_id", "directory_coopaddresstags"."address_id", "directory_coopaddresstags"."is_public" FROM "directory_coopaddresstags" WHERE "directory_coopaddresstags"."coop_id" = 271; args=(271,)
(0.000) SELECT "directory_coopaddresstags"."id", "directory_coopaddresstags"."coop_id", "directory_coopaddresstags"."address_id", "directory_coopaddresstags"."is_public" FROM "directory_coopaddresstags" WHERE "directory_coopaddresstags"."coop_id" = 271; args=(271,)
type of instance: <class 'directory.models.CoopAddressTags'>
(0.000) SELECT "address_address"."id", "address_address"."street_number", "address_address"."route", "address_address"."locality_id", "address_address"."raw", "address_address"."formatted", "address_address"."latitude", "address_address"."longitude" FROM "address_address" WHERE "address_address"."id" = 263 LIMIT 21; args=(263,)
(0.000) SELECT "address_locality"."id", "address_locality"."name", "address_locality"."postal_code", "address_locality"."state_id" FROM "address_locality" WHERE "address_locality"."id" = 16 LIMIT 21; args=(16,)
(0.000) SELECT "address_state"."id", "address_state"."name", "address_state"."code", "address_state"."country_id" FROM "address_state" WHERE "address_state"."id" = 19313 LIMIT 21; args=(19313,)
(0.000) SELECT "address_country"."id", "address_country"."name", "address_country"."code" FROM "address_country" WHERE "address_country"."id" = 484 LIMIT 21; args=(484,)

Таким образом, если есть 6 результатов, то вышеуказанный запрос выполняется 6 раз с разными идентификаторами. Есть ли способ выполнить только один набор запросов, чтобы независимо от того, есть ли 1, 10 или 100 результатов, было выполнено одинаковое количество запросов для возврата данных?

Я вижу, вы собираете данные из многих M2M и FK отношений.

Я согласен с @Amres, вы можете использовать prefetch_related и select_related в ваших запросах, но простое использование этих методов не поможет вам.

Первый:

При использовании prefetch_related и select_related можно добиться 3 запросов на запрос. Не ниже. 1 большой запрос и 2 для m2m отношений. Подробнее здесь: https://docs.djangoproject.com/en/4.1/ref/models/querysets/#select-related. https://docs.djangoproject.com/en/4.1/ref/models/querysets/#prefetch-related

Секунда:

Вы не можете использовать просто:

.prefetch_related('addresses')

ничего не делает. В вашем случае необходимо использовать объект Prefetch. Подробнее здесь: https://docs.djangoproject.com/en/4.1/ref/models/querysets/#django.db.models.Prefetch

Почему? Ответ - у вас есть дополнительные FK в адресной модели:

  • адреса__raw,
  • addresses__locality,
  • addresses__locality__state,
  • addresses__locality__state__country

Как он может быть объявлен:

addressPrefetcher = Prefetch('adresses', queryset=Address.objects.select_related('raw', 'locality', 'locality__state', 'locality__state__country'))

После этого

Вы можете использовать эту конструкцию:

coops = Coop.objects.find(Your_query).select_related('phone', 'email').prefetch_related(addressPrefetcher, 'types')

Если я действительно правильно понимаю ваши модели - вы получаете только 3 попадания в БД, независимо от того, сколько объектов в CoopSearchSerializer. Вы можете получить только 1 попадание с помощью GroupConcat, но для данного бизнес случая это накладно.

p.s. Возможно, сохранить phone и email как FK, не очень хорошая идея. Но это возможно.

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