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.
Мои вопросы следующие:
Я думал, что префетчинг
d_objs__C
предотвратит это?Не менее интересно, почему отсутствие комментария
(*)
и оставление этой строки удваивает количество запросов?!Не важно, но что случилось с
LIMIT 21
в этом запросе? Интересно, это подсказка к тому, что происходит.
Очевидно, мое понимание prefetch_related
неправильно. Заранее спасибо.
Оказалось, что моя select_related
была правильной. Виной тому был приемник post_init
на B
, который вызывал self.b.a
, что приводило к отдельным запросам для каждого объекта A
при каждом инстанцировании объекта B
. Болезненный урок усвоен.
Для тех, кто столкнулся с подобными проблемами, я смог отладить это с помощью этого замечательного инструмента SQL stacktrace, который печатает python stacktrace того, где генерируется запрос: https://github.com/dobarkod/django-queryinspect.