Saltar al contenido
🐍 Curso de Python
/

Módulo 5 · Lección 14 de 30

ModelSerializer y relaciones

Cuatro líneas que generan once campos, y el listado que pasa de 2 consultas a 399 por enseñar el nombre de la editorial.

⏱ 17 min de lectura

ModelSerializer escribe el serializador por ti a partir del modelo. Ahorra mucho código y es lo que se usa el 90 % de las veces.

Y las relaciones son la parte difícil de una API: decidir si la editorial de un libro sale como número o como objeto no es cosmético, es la diferencia entre dos consultas y trescientas noventa y nueve.

Lo que genera#

Cuatro líneas:

python
class LibroSerializer(serializers.ModelSerializer):
        class Meta:
                model = Libro
        fields = '__all__'

Y esto es lo que construye, que se puede ver imprimiendo una instancia:

Salida en pantalla
LibroSerializer():
    id = BigIntegerField(label='ID', read_only=True)
    titulo = CharField(label='Título', max_length=200)
    isbn = CharField(label='ISBN', max_length=17,
                     validators=[<UniqueValidator(queryset=Libro.objects.all())>])
    precio = DecimalField(decimal_places=2, max_digits=7)
    paginas = IntegerField(label='Páginas', max_value=..., min_value=0, required=False)
    estado = ChoiceField(choices=[('borrador', 'Borrador'), ...], required=False)
    publicado_en = DateField(allow_null=True, required=False)
    creado = DateTimeField(read_only=True)
    actualizado = DateTimeField(read_only=True)
    editorial = PrimaryKeyRelatedField(queryset=Editorial.objects.all())
    autores = PrimaryKeyRelatedField(allow_empty=False, many=True,
                                     queryset=Autor.objects.all())

Imprimir el serializador es la herramienta de depuración más útil de DRF y casi nadie la conoce. Enseña exactamente qué campos hay, con qué tipo y con qué reglas, que es la respuesta a la mitad de las preguntas de "por qué esto no valida".

Cuatro cosas que ha deducido solo:

Las etiquetas salen del verbose_name. Título, Páginas, ISBN, con sus tildes. Un modelo escrito en español da una API documentada en español gratis.

El unique=True del modelo se convierte en un validador, así que un ISBN repetido da un 400 limpio en vez de un error de base de datos.

Los campos con auto_now son de solo lectura. Bien deducido.

Las relaciones salen como identificadores, y eso es lo que vamos a cambiar.

Las cinco formas de una relación#

Sobre el mismo libro, lo que devuelve cada una:

Salida en pantalla
PrimaryKeyRelatedField  {"editorial": 2, "autores": [1]}
StringRelatedField      {"editorial": "Alfaguara", "autores": ["Óscar Núñez"]}
SlugRelatedField        {"editorial": "Alfaguara", "autores": ["Óscar Núñez"]}
HyperlinkedRelatedField {"editorial": "http://.../api/editoriales/2/"}
serializador anidado    {"editorial": {"id": 2, "nombre": "Alfaguara", "pais": "España"},
                         "autores": [{"id": 1, "nombre": "Óscar Núñez"}]}

Cuándo usar cada una:

FormaEscribibleCuándo
Por identificadorSíLo predeterminado; el cliente ya tiene los datos
Por textoNoSolo lectura, rápido de leer
Por campoSíCuando hay un identificador natural: código, ISBN
Por enlaceSíAPIs que quieren ser navegables
AnidadoNo, de entradaCuando quien la usa necesita los datos y no otra petición

Por campo es la más infravalorada. Si tus editoriales tienen un código único, SlugRelatedField(slug_field='codigo') da una API en la que "editorial": "anagrama" se lee y se escribe, sin que nadie tenga que saber que la editorial es el número 2.

Anidado es lo que casi siempre quiere quien consume la API, y es lo que cuesta.

Lo que cuesta cada una#

Aquí está el número de la lección. Serializando doscientos libros:

Salida en pantalla
solo campos propios        1 consultas
ids (sin optimizar)      200 consultas
ids (con prefetch)         2 consultas
anidado sin optimizar    399 consultas
anidado optimizado         2 consultas

Lo que sorprende es la segunda línea: devolver solo los identificadores de los autores ya cuesta una consulta por libro. La intuición dice que un número es gratis, y no lo es: es una relación de muchos a muchos, y para saber qué identificadores tiene cada libro hay que ir a la tabla intermedia.

Y de ahí la conclusión práctica: la optimización no depende de la forma que elijas, depende de que optimices. Anidado y optimizado cuesta lo mismo que identificadores y optimizado: dos consultas.

El arreglo va en la vista, no en el serializador:

python
class LibroViewSet(viewsets.ModelViewSet):
        queryset = (Libro.objects
                .select_related('editorial')        # clave ajena: una unión
                .prefetch_related('autores'))       # muchos a muchos: otra consulta
    serializer_class = LibroSerializer

Es exactamente lo de la lección 15 del módulo 4, y aquí duele más porque en una web solo se ven veinte filas de golpe y en una API es normal pedir cien.

Escribir relaciones#

Leer anidado es fácil. Escribir anidado no funciona:

python
class LibroSerializer(serializers.ModelSerializer):
        editorial = EditorialSerializer()
Salida en pantalla
is_valid():  True
AssertionError: The `.create()` method does not support writable nested fields
by default. Write an explicit `.create()` method for serializer `LibroSerializer`,
or set `read_only=True` on nested serializer fields.

Fíjate en que valida bien y revienta al guardar. DRF no sabe qué quieres: ¿crear una editorial nueva? ¿buscar la que se parezca? ¿actualizar la que hay? Son decisiones tuyas, y hace bien en no adivinar. El mensaje, además, dice las dos salidas.

La solución habitual, y la que conviene por defecto, es dos campos: uno para leer y otro para escribir.

python
class LibroSerializer(serializers.ModelSerializer):
        editorial = EditorialSerializer(read_only=True)
    editorial_id = serializers.PrimaryKeyRelatedField(
        source='editorial',
        queryset=Editorial.objects.all(),
        write_only=True,
    )

    class Meta:
                model = Libro
        fields = ['id', 'titulo', 'isbn', 'precio',
                  'editorial', 'editorial_id', 'autores']
Salida en pantalla
POST {"titulo": "Nuevo", "isbn": "978-99-XYZ", "precio": "10.00",
      "editorial_id": 1, "autores": [1, 2]}

201  {"id": 201, "titulo": "Nuevo", "precio": "10.00",
      "editorial": {"id": 1, "nombre": "Anagrama", "pais": "España"},
      "autores": [1, 2]}

Se escribe un número y se lee un objeto, que es lo que quiere todo el mundo. Y es la convención de nombres de la lección 11: el sufijo distingue el identificador del objeto.

El source='editorial' es la pieza que lo hace funcionar: dice que ese campo, aunque se llame editorial_id de cara afuera, escribe en el atributo editorial del modelo.

Si de verdad necesitas crear la editorial junto con el libro, se escribe el create a mano:

python
def create(self, validated_data):
        datos_editorial = validated_data.pop('editorial')
    editorial, _ = Editorial.objects.get_or_create(**datos_editorial)
    return Libro.objects.create(editorial=editorial, **validated_data)

Y conviene pensarlo dos veces, porque una API que crea cosas de rebote es difícil de razonar: un error de escritura en el nombre de la editorial crea una editorial nueva en vez de dar un error.