Вложенные сериалайзеры против SQL-запросов в Django REST Framework
- четверг, 4 июля 2019 г. в 00:19:11
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',)
Если это решение кажется слишком топорным, то вы можете ознакомится со статьей, которая побудила меня написать этот пост.