Django: prefetch_related во вложенных сериализаторах не уменьшает тысячи запросов к БД, а может даже увеличить их количество

У меня странная ситуация. Я много читал о том, как избежать проблемы N+1 и пробовал префетчинг, но безрезультатно. Моя установка выглядит следующим образом:

Модели:

class A(models.Model):
    # some stuff

class B(models.Model):
    a = models.ForeignKey(A, on_delete=models.CASCADE, related_name="b_objs")

class C(models.Model):
    somefield1 = models.CharField(max_length=100)
    somefield2 = models.CharField(max_length=100)

    # Assume that classes Y and Z exist. We don't care about them.
    # In the serializer, the fields don't include 'y' and 'z'
    y = models.ForeignKey(Y, on_delete=models.CASCADE)
    z = models.ForeignKey(Z, on_delete=models.CASCADE)

class D(models.Model)
    b = models.ForeignKey(B, on_delete=models.CASCADE, related_name="d_objs")
    c = models.ForeignKey(C, on_delete=models.CASCADE)

Визуально:

D --> B --> A
D --> C --> Y and Z (Again, we don't care about C's FKs to Y and Z)

Есть 1 A object, 8 B objects, указывающие на A, 188 C objects и 1291 D objects, указывающие на некоторую комбинацию этих 8 B и 188 C. Когда я сериализую объект A, он распространяется вниз и сериализует все эти объекты.

Сериализаторы выглядят следующим образом:

class CSerializer(serializers.ModelSerializer):
    class Meta:
        model = C
        fields = ['somefield1', 'somefield1']

class DSerializer(serializers.ModelSerializer):
    c = CSerializer()

    class Meta:
        model = D
        fields = '__all__'

class BSerializer(serializers.ModelSerializer):
    d_objects = serializers.SerializerMethodField(method_name='get_d_objects')

    class Meta:
        model = B
        fields = '__all__'

    def get_d_objects(self, obj):
        return DSerializer(obj.all_d_objs, many=True).data # 'all_d_objs' is from the Prefetch to_attr below


class ASerializer(serializers.ModelSerializer):
    b_objects = serializers.SerializerMethodField(method_name='get_b')

    class Meta:
        model = A
        fields = '__all__'

    def get_b(self, obj):
        qs = obj.b_objs.prefetch_related(
                Prefetch(
                    'd_objs',  # the related_name on D's FK to B
                    to_attr='all_d_objs',
                ),
                'd_objs__C' # (*)
            )
        return BSerializer(qs, many=True).data

Он выполняет около 1317 запросов, если я закомментирую строку (*) и примерно вдвое больше, если я раскомментирую (*).

Когда я регистрирую свои запросы к БД django (https://stackoverflow.com/a/20161527/194707), я вижу более 1000 вызовов для выбора отдельных элементов C следующим образом (скорее всего, потому что у нас есть 1291 объект D, каждый из которых указывает на один объект C):

(0.000) SELECT "C"."id",
               "C"."somefield1",
               "C"."somefield2",
               "C"."Y_id",
               "C"."Z_id"
               FROM "C" WHERE "C"."id" = 109 LIMIT 21; args=(109,); alias=default

Значение C.id (109) меняется при каждом запросе, предположительно для каждого объекта D, который указывает на один объект C. Обратите также внимание, что он запрашивает C.Y_id и C.Z_id, хотя эти поля не включены в fields сериализатора C.

Мои вопросы следующие:

  1. Я думал, что префетчинг d_objs__C предотвратит это?

  2. Не менее интересно, почему отсутствие комментария (*) и оставление этой строки удваивает количество запросов?!

  3. Не важно, но что случилось с LIMIT 21 в этом запросе? Интересно, это подсказка к тому, что происходит.

Очевидно, мое понимание prefetch_related неправильно. Заранее спасибо.

Оказалось, что моя select_related была правильной. Виной тому был приемник post_init на B, который вызывал self.b.a, что приводило к отдельным запросам для каждого объекта A при каждом инстанцировании объекта B. Болезненный урок усвоен.

Для тех, кто столкнулся с подобными проблемами, я смог отладить это с помощью этого замечательного инструмента SQL stacktrace, который печатает python stacktrace того, где генерируется запрос: https://github.com/dobarkod/django-queryinspect.

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