django

Вложенные сериалайзеры против SQL-запросов в Django REST Framework

  • четверг, 4 июля 2019 г. в 00:19:11
https://habr.com/ru/post/458722/
  • Python
  • Django
  • SQL


Django REST Framework позволяет быстро создавать интерфейсы, но в случае со вложенными сериалайзерами нужно быть аккуратным.


Описание проблемы


Представим, что у нас есть таблица с абстрактными карточками:


# card/models.py
class Card(models.Model):
    owner = models.ForeignKey('user.User', on_delete=models.CASCADE)
    name = models.CharField(max_length=250)
    description = models.TextField(null=True, blank=True)
    start_date = models.DateTimeField()
    ...

Вьюха:


# card/views.py
class CardList(generics.ListCreateAPIView):
    queryset = Card.objects.all()
    serializer_class = CardListSerializer
    pagination_class = DefaultResultsSetPagination
    ...

Ну и конечно же сериалайзеры:


# card/serializers.py
class OwnerCardSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('id', 'username',)

class CardListSerializer(serializers.ModelSerializer):
    owner = OwnerCardSerializer()  # это убивает производительность
    class Meta:
        model = Card
        fields = ('owner', 'category', 'name', 'description', 'start_date', 'lifetime', 'image', 'image_thumb',)

Проблема в том, что каждая новая карточка в списке порождает отдельный sql-запрос. Мы можем это проверить изменив вьюху:


from django.db import connection

class CardList(generics.ListCreateAPIView):
    queryset = Card.objects.all()
    serializer_class = CardListSerializer
    pagination_class = DefaultResultsSetPagination

    def list(self, request, *args, **kwargs):
        objects = super().list(request, *args, **kwargs)
        print(len(connection.queries))  # вывод кол-ва запросов
        print(connection.queries)  # сами запросы
        return objects

У меня в таблице три карточки и помимо стандартных запросов (определение пользователя, определение количества карточек и непосредственно вывод самих карточек) мы увидим еще три запроса:


[...
    {'sql': 'SELECT "user"."id", "user"."password", ... FROM "user" WHERE "user"."id" = 3', 'time': '0.000'},
    {'sql': 'SELECT "user"."id", "user"."password", ... FROM "user" WHERE "user"."id" = 4', 'time': '0.000'},
    {'sql': 'SELECT "user"."id", "user"."password", ... FROM "user" WHERE "user"."id" = 5', 'time': '0.000'},
]

Хорошо, что у нас есть пагинация


Выходим из положения


Тут нам нужно написать запрос с join-ом, чтобы получилось примерно так:


SELECT c.*, u.username as owner_username
FROM card c
JOIN user u on c.owner_id = u.id

Таким образом мы не будем плодить лишние запросы, ведь каждый username берется из join-а. Но таким образом у нас не будет вложенности, сейчас это исправим.
Первым делом опишем с помощью Django ORM запрос выше через F():


# card/models.py
class Card(models.Model):
    owner = models.ForeignKey('user.User', on_delete=models.CASCADE)
    name = models.CharField(max_length=250)
    description = models.TextField(null=True, blank=True)
    start_date = models.DateTimeField()
    ...
    @classmethod
    def get_all(cls, coords=None):
        objects = cls.objects.annotate(owner_username=F('owner__username'))

И изменим способ получения объектов во вьюхе:


# card/views.py
class CardList(generics.ListCreateAPIView):
    queryset = Card.get_all()
    ...

Я хочу, чтобы можно было очень просто добавить вложенность, поэтому определим это прямо в сериалайзере:


# card/serializers.py
class CardListSerializer(serializers.ModelSerializer):
    owner_username = serializers.CharField()  # опишем новое поле
    class Meta:
        model = Card
        fields = ('owner_id', 'owner_username', 'category', 'name', 'description', 'start_date', 'lifetime', 'image', 'image_thumb',)
        # таким образом мы будем обозначать вложенность, а ориентироваться на название ('owner_id', 'owner_username')
        nested_fields = ('owner',)

Теперь осталось переопределить ModelSerializer под наши новые требования. Сделаем это через to_representation:


rest/serializers.py
class NestedModelSerializer(serializers.ModelSerializer):
    def to_representation(self, instance):
        data = super().to_representation(instance)  # берем данные по умолчанию
        new_data = data.copy()
        if hasattr(self.Meta, 'nested_fields'):
            for nested_field in self.Meta.nested_fields:  # наши nested_fields
                for field in data:
                    # в зависимости от вашего делителя, находим вложенные поля
                    if field.startswith(f'{nested_field}_'):
                        if nested_field not in new_data:
                            # создаем вложенную структуру, если ее нет
                            new_data[nested_field] = OrderedDict()
                        # берем название поля ('owner_username' -> 'username')
                        field_name = field.split('_', maxsplit=1)[-1]
                        new_data[nested_field][field_name] = new_data.pop(field)
        return new_data

И, конечно же, не забываем про CardListSerializer, изменив на новый наследуемый класс


# card/serializers.py
from apps.rest.serializers import NestedModelSerializer

class CardListSerializer(NestedModelSerializer):
    owner_username = serializers.CharField()
    class Meta:
        model = Card
        fields = ('owner_id', 'owner_username', 'category', 'name', 'description', 'start_date', 'lifetime', 'image', 'image_thumb',)
        nested_fields = ('owner',)

Если это решение кажется слишком топорным, то вы можете ознакомится со статьей, которая побудила меня написать этот пост.