Los serializadores son unos de los componentes más poderosos que tiene Django Rest Framework. Estos permiten que estructuras complejas y modelos de nuestro proyecto en Django sean convertidos a estructuras nativas de Python y puedan ser convertidas fácilmente en JSON
o XML
. El uso de ellos es muy extendido y uno de los temas claves que tocaremos en este post es la optimización de serializadores.
Imaginemos que tenemos una estructura básica de Country
y City
donde una ciudad pertenece a un país, así:
# models.py
from django.db import models
classCountry(models.Model):
name = models.CharField(max_length=55)
classCity(models.Model):
name = models.CharField(max_length=55)
country = models.ForeignKey(
Country, related_name='cities', on_delete=models.SET_NULL)
El viewsets
es el lugar donde declaramos la lógica de nuestra vista, con Django Rest Framework
en muy pocas líneas declararemos lo necesario para que funcione nuestra lógica
# viewsets.py
from rest_framework import viewsets
from .models import Country
from .serializers import CountrySerializer
classCountryViewSet(viewsets.ReadOnlyModelViewSet):
...
queryset = Country.objects.all()
serializer_class = CountrySerializer
model = Country
...
En los serializadores es donde escribiremos la lógica de la estructura final que será la respuesta al request que haremos a nuestro proyecto
# serializers.pyfrom rest_framework import serializers
from .models import Country, City
classCitySerializer(serializers.ModelSerializer):classMeta:
model = City
fields = (
'id',
'name',
)
classCountrySerializer(serializers.ModelSerializer):
cities = CitySerializer(many=True)
classMeta:
model = Country
fields = (
'id',
'name',
'cities',
)
Antes de continuar analicemos más a fondo lo que tenemos escrito más arriba:
City
y Country
, donde City
contiene el foreign key
a Country
ViewSet
donde declaramos en el atributo queryset
, la sentencia que nos buscará todos registros existentes de Country
y que los serializará con CountrySerializer
CountrySerializer
pedimos que nos muestre los datos según la estructura mostrada en el atributo fields
. Lo importante acá, y nos servirá luego para hacer la optimización, es el nested serializer
que declaramos para el atributo cities
. Esto básicamente indica que todas las cities
asociadas al Country
serán serializadas con CitySerializer
Si por ejemplo hacemos una petición GET al endpoint country/1/
tendremos la siguiente estructura de respuesta:
{
"id": 1,
"name": "Colombia",
"cities": [
{
"id": 1,
"name": "Bogotá"
},
{
"id": 2,
"name": "Medellín"
},
{
"id": 3,
"name": "Cali"
}
]
}
La lógica que declaramos en los serializadores nos permite tener estructuras muy ordenadas como esta que obtuvimos, pero cuando nuestro proyecto es muy grande y procesa una cantidad de requests alta es necesario pensar en todos los queries
que se hacen a la base de datos.
En este caso y con esta respuesta tenemos los siguientes queries:
country
de id=1
city
que tienen como foreign key=1
En total ejecutamos 4 queries a la base de datos para obtener la información. Este problema es muy común en programación y se llama el problema del N+1
, donde 1
representa al objeto base de la consulta (en este caso Country
) y N
representa a la N cantidad de atributos relacionados a él (en nuestro caso City
).
Eager Loading
es un concepto muy utilizado para resolver el problema del N+1
, básicamente disminuye las consultas con un proceso más inteligente para hacer queries a la base de datos; en nuestro caso, en vez de hacer cuatro peticiones para solicitar la información de City
hace una sola consulta y precarga la información de estas.
Django
permite utilizar eager loading
a través del método prefetch_related
, este es el que implementaremos para optimizar las consultas. El cambio no es tan grande a nivel de código, solo tenemos que sobreescribir el método get_queryset
de CountryViewSet
, así:
# viewsets.pyfrom rest_framework import viewsets
from django.db.models import Prefetch
from .models import Country
from .serializers import CountrySerializer
classCountryViewSet(viewsets.ReadOnlyModelViewSet):
...
queryset = Country.objects.all()
serializer_class = CountrySerializer
...
defget_queryset(self):
queryset = super().get_queryset()
queryset = queryset.prefetch_related(
Prefetch('cities')
)
return queryset
Sobreescribir el método get_queryset
nos permite ajustar el comportamiento y modificar la consulta conforme a lo que necesitemos, en este caso, que nos precargue los datos de cities
asociados al Country
que se consulta.
Si hacemos de nuevo el llamado al endpoint country/1/
no notaremos cambios en el JSON de la respuesta, pero sí habrá una disminución en la cantidad de queries a la base de datos, en lugar de las 4 que teníamos al inicio se disminuyó a 2, dado que la primera consulta es para traer la información de Country
y la segunda es para traer la información de las ciudades asociadas.
De esta forma y aplicando unos cambios muy pequeños en la lógica de nuestro código podemos mejorar el rendimiento y la velocidad de respuesta del proyecto.
En el Curso Avanzado de Django podrás aprender muchos conceptos como este y tendrás una aplicación tan rápida que tus usuarios amarán usarla.
Excelente post! Una pregunta, de qué forma puedo ver cuántas cosultas se hacen a la base de datos?
Preferiblemente una que pueda ejecutar desde el shell interactivo de Django!
Creo que en este ejemplo se debió utilizar el comando select_related() el prefetch related() se utilizaría cuando realizarías la consulta en Many To Many, con el seletc_related() se haría la consulta con los inner joins correspondientes
defget_queryset(self): queryset = super().get_queryset() queryset = queryset.prefetch_related('cities') return queryset
Que alguien me corrija si me equivoco, saludos!
El
select_related
se usaría si quisiéramos traer el país asociado a la ciudad, como en este caso son las ciudades asociadas al país usamosprefetch_related
como puedo trabajar imagenes con los serializadores
Excelent post.
Btw, está roto el link “Curso Avanzado de Django”
Excelente artículo. ¡Gracias!
Increíble 😄