Django ORM Оптимизация запроса
КОД:
Модель Answers:
class Answers(models.Model):
"""
Класс модели ответа
"""
content = models.TextField( verbose_name = "Содержимое ответа", max_length = 4096, )
date_of_creation = models.DateTimeField( verbose_name = "Дата создания ответа", auto_now_add = True, )
is_correct = models.BooleanField( verbose_name = "Наличие статуса 'правильный ответ' у ответа", default = False, )
phor = models.ForeignKey( verbose_name = "Ссылка на фор ответа", to = Phors, on_delete = models.CASCADE, related_name = 'answers' )
creator = models.ForeignKey( verbose_name = "Ссылка на создателя ответа", to = UsersOfClient, on_delete = models.CASCADE, related_name = 'answers' )
parent_answer = models.ForeignKey( verbose_name = "Ссылка на родительский ответ ответа", to = 'self', on_delete = models.CASCADE, blank = True, null = True, related_name = 'child_answers' )
def __str__(self) -> str:
return f'Ответ от {self.creator.username} для фора {self.phor.title} | PK : {self.pk}'
class Meta:
ordering = [ '-date_of_creation', ]
verbose_name = 'Ответ'
verbose_name_plural = 'Ответы'
Модель Phors:
class Phors(models.Model):
"""
Класс модели фора
"""
title = models.CharField( verbose_name = "Заголовок фора", max_length = 128, unique = True )
description = models.TextField( verbose_name = "Описание проблемы в форе", max_length = 4096, )
date_of_creation = models.DateTimeField( verbose_name = "Дата создания фора", auto_now_add = True, )
theme = models.ForeignKey( verbose_name = "Ссылка на тему фора", to = Themes, on_delete = models.CASCADE, related_name = 'phors' )
creator = models.ForeignKey( verbose_name = "Ссылка на создателя фора", to = UsersOfClient, on_delete = models.SET_NULL, null = True, related_name = 'phors' )
slug = models.SlugField( verbose_name = "Слаг фора", max_length = 256, unique = True, db_index = True, )
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse( 'phor', kwargs = { 'slug_of_phor' : self.slug, 'slug_of_theme' : self.theme.slug } )
class Meta:
ordering = [ '-date_of_creation', 'title' ]
verbose_name = 'Фор'
verbose_name_plural = 'Форы'
Сериализаторы для Answers:
class _FilterAnswerSerializer( serializers.ListSerializer ):
""" Класс сериализатора, который убирает из общего списка Answers записи c parent_answer != None """
def to_representation(self, data):
data = data.filter( parent_answer = None )
return super().to_representation(data)
class _ChildAnswerSerializer( serializers.Serializer ):
""" Сериализатор для рекурсии и отображения дочерних Answers """
def to_representation(self, value):
serializer = _AnswerSerializer( value, context = self.context )
return serializer.data
class _AnswerSerializer( serializers.ModelSerializer ):
""" Сериализатор для Answers """
creator = serializers.SlugRelatedField( slug_field = 'username', read_only = True)
child_answers = _ChildAnswerSerializer( many = True )
class Meta:
list_serializer_class = _FilterAnswerSerializer
model = Answers
fields = ( 'creator', 'date_of_creation', 'is_correct', 'content', 'child_answers', 'pk' )
Сериализаторы для Phors:
class _ListPhorSerializer( serializers.ModelSerializer ):
""" Сериализатор для экземпляров Phors модели """
creator = serializers.SlugRelatedField( slug_field = 'username', read_only = True)
class Meta:
model = Phors
fields = ( 'title', 'slug', 'creator', )
class PhorSerializer( serializers.ModelSerializer ):
""" Сериализатор для экземпляра Phors модели """
answers = _AnswerSerializer( many = True )
theme = serializers.SlugRelatedField( slug_field = 'slug', read_only = True )
creator = serializers.SlugRelatedField( slug_field = 'username', read_only = True)
class Meta:
model = Phors
fields = ( 'title', 'creator', 'theme', 'date_of_creation', 'slug', 'description', 'answers', 'pk' )
Получаемый queryset в ViewSet для модели фор
queryset = Phors.objects.all().select_related( 'creator', 'theme', )
**Объяснение: **
ViewSet при Retrieve отдаёт фор ( заданный вопрос ) к которому привязанны ответы, и к этим ответам привязанны дочерние ответы.
Получаемый результат:
{
"title": "TooT",
"creator": "bob3_user_3",
"theme": "python-dlya-nachinayushih",
"date_of_creation": "2022-07-14T01:56:40.270240Z",
"slug": "toot",
"description": "PROOOOBLEM",
"answers": [
{
"creator": "dodge1",
"date_of_creation": "2022-07-16T20:43:10.349158Z",
"is_correct": false,
"content": "ewqewq",
"child_answers": [],
"pk": 15
},
{
"creator": "dodge1",
"date_of_creation": "2022-07-16T19:52:39.536253Z",
"is_correct": false,
"content": "fdfdf",
"child_answers": [],
"pk": 12
},
{
"creator": "bob55",
"date_of_creation": "2022-07-16T19:50:34.190748Z",
"is_correct": false,
"content": "rrrr",
"child_answers": [
{
"creator": "dodge1",
"date_of_creation": "2022-07-16T19:53:01.082159Z",
"is_correct": false,
"content": "ffff",
"child_answers": [],
"pk": 13
}
],
"pk": 11
},
{
"creator": "dodge1",
"date_of_creation": "2022-07-16T19:48:26.858972Z",
"is_correct": false,
"content": "rerwerwr",
"child_answers": [],
"pk": 10
},
{
"creator": "bob3_user_2",
"date_of_creation": "2022-07-14T11:51:49.190718Z",
"is_correct": false,
"content": "rrr",
"child_answers": [
{
"creator": "dodge1",
"date_of_creation": "2022-07-16T19:20:54.982285Z",
"is_correct": false,
"content": "Ответище",
"child_answers": [],
"pk": 9
}
],
"pk": 7
},
{
"creator": "bob3_user_3",
"date_of_creation": "2022-07-14T11:50:48.437105Z",
"is_correct": false,
"content": "ror",
"child_answers": [
{
"creator": "bob3_user_2",
"date_of_creation": "2022-07-14T11:52:04.385385Z",
"is_correct": false,
"content": "dead",
"child_answers": [],
"pk": 8
}
],
"pk": 6
}
],
"pk": 30
}
Запросы:
SELECT ••• FROM "django_session" WHERE ("django_session"."expire_date" > '''2022-07-16 20:58:02.316319''' AND "django_session"."session_key" = '''vdlr8lo54kk5mu1nodoa0negvxij03m8''') LIMIT 21
SELECT ••• FROM "auth_user" WHERE "auth_user"."id" = '2' LIMIT 21
SELECT ••• FROM "forum_phors" INNER JOIN "forum_themes" ON ("forum_phors"."theme_id" = "forum_themes"."id") LEFT OUTER JOIN "forum_usersofclient" ON ("forum_phors"."creator_id" = "forum_usersofclient"."id") WHERE "forum_phors"."slug" = '''toot''' LIMIT 21
SELECT ••• FROM "forum_answers" WHERE ("forum_answers"."phor_id" = '30' AND "forum_answers"."parent_answer_id" IS NULL) ORDER BY "forum_answers"."date_of_creation" DESC
SELECT ••• FROM "forum_usersofclient" WHERE "forum_usersofclient"."id" = '27' LIMIT 21
9 similar queries. Duplicated 5 times.
SELECT ••• FROM "forum_answers" WHERE "forum_answers"."parent_answer_id" = '15' ORDER BY "forum_answers"."date_of_creation" DESC
9 similar queries.
SELECT ••• FROM "forum_usersofclient" WHERE "forum_usersofclient"."id" = '27' LIMIT 21
9 similar queries. Duplicated 5 times.
SELECT ••• FROM "forum_answers" WHERE "forum_answers"."parent_answer_id" = '12' ORDER BY "forum_answers"."date_of_creation" DESC
9 similar queries.
SELECT ••• FROM "forum_usersofclient" WHERE "forum_usersofclient"."id" = '28' LIMIT 21
9 similar queries.
SELECT ••• FROM "forum_answers" WHERE "forum_answers"."parent_answer_id" = '11' ORDER BY "forum_answers"."date_of_creation" DESC
9 similar queries.
SELECT ••• FROM "forum_usersofclient" WHERE "forum_usersofclient"."id" = '27' LIMIT 21
9 similar queries. Duplicated 5 times.
SELECT ••• FROM "forum_answers" WHERE "forum_answers"."parent_answer_id" = '13' ORDER BY "forum_answers"."date_of_creation" DESC
9 similar queries.
SELECT ••• FROM "forum_usersofclient" WHERE "forum_usersofclient"."id" = '27' LIMIT 21
9 similar queries. Duplicated 5 times.
SELECT ••• FROM "forum_answers" WHERE "forum_answers"."parent_answer_id" = '10' ORDER BY "forum_answers"."date_of_creation" DESC
9 similar queries.
SELECT ••• FROM "forum_usersofclient" WHERE "forum_usersofclient"."id" = '23' LIMIT 21
9 similar queries. Duplicated 2 times.
SELECT ••• FROM "forum_answers" WHERE "forum_answers"."parent_answer_id" = '7' ORDER BY "forum_answers"."date_of_creation" DESC
9 similar queries.
SELECT ••• FROM "forum_usersofclient" WHERE "forum_usersofclient"."id" = '27' LIMIT 21
9 similar queries. Duplicated 5 times.
SELECT ••• FROM "forum_answers" WHERE "forum_answers"."parent_answer_id" = '9' ORDER BY "forum_answers"."date_of_creation" DESC
9 similar queries.
SELECT ••• FROM "forum_usersofclient" WHERE "forum_usersofclient"."id" = '24' LIMIT 21
9 similar queries.
SELECT ••• FROM "forum_answers" WHERE "forum_answers"."parent_answer_id" = '6' ORDER BY "forum_answers"."date_of_creation" DESC
9 similar queries.
SELECT ••• FROM "forum_usersofclient" WHERE "forum_usersofclient"."id" = '23' LIMIT 21
9 similar queries. Duplicated 2 times.
SELECT ••• FROM "forum_answers" WHERE "forum_answers"."parent_answer_id" = '8' ORDER BY "forum_answers"."date_of_creation" DESC
9 similar queries.
Подробнее:
Я понимаю что есть проблема в рекурсии. Я могу использовать prefetch_related с аргументов в виде класса Prefetch что бы не тратить запрос на отображение имени создателя ( creator ) у ответа, но тогда, для каждой вложенности дочерних ответов нужен отдельный prefetch_related. Я могу избавиться от этого перестав отображать имя создателя ответа, и возможно к этому прийдется прибегнуть, но это не суть вопроса, хотя обход этого дела не отказался бы понять и узнать.
Суть:
Можно заметить, что для каждой ответа, для каждой группы дочерних ответов создаётся дополнительный sql запрос, и так-же это касается самого answers, хотя это и не так критично.
Я бы хотел понять, как я могу все это решить, и правильно организовать запрос обращаясь с вторичных моделей к первичной. Надеюсь я был понят, заранее спасибо.
При отображении записи из модели Phors вместо отображения Answers в том виде в котором оно есть сейчас, я буду выводить список первичных ключей к связанным ответам, и добавлю метод retrieve для ViewSet Answers. Таким способом я избавлю себя от сериализаторов связанных с рекурсией, избавлюсь от тоны запросов в виде ошибки n + 1, в замен добавлю необходимость в 1 request запросе для клиента к ответам принадлежащим к Фору.
Однако, данных подход влечет за собой две проблемы, одна из которых нарушает основной принцип этого проекта: Бэкенд связанный с отображением, созданием или изменением записей берется на сам проект. Но таким решением, мало того что появляется необходимость в дополнительном request запросе, так и от ошибки n + 1 не избавится, не убрав рекурсию, а убрав рекурсию, клиенту нужно будет вычислять иерархию ответов на своей стороне.
- Небольшая поправка, я временно убрал рекурсию, без необходимости в отдельном методе retrieve для Answers *