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 *
Вернуться на верх