DRF: asignación de clave externa simple con serializadores nesteds?

Con Django REST Framework, un ModelSerializer estándar permitirá que las relaciones de modelo de ForeignKey se asignen o modifiquen mediante la POST de una ID como un entero.

¿Cuál es la forma más sencilla de obtener este comportamiento de un serializador nested?

Tenga en cuenta que solo estoy hablando de asignar objetos de base de datos existentes, no de creación anidada.

En el pasado, he eliminado esto con campos ‘id’ adicionales en el serializador y con métodos personalizados de create y update , pero este es un problema aparentemente simple y frecuente para mí que tengo curiosidad por saber cuál es la mejor manera.

 class Child(models.Model): name = CharField(max_length=20) class Parent(models.Model): name = CharField(max_length=20) phone_number = models.ForeignKey(PhoneNumber) child = models.ForeignKey(Child) class ChildSerializer(ModelSerializer): class Meta: model = Child class ParentSerializer(ModelSerializer): # phone_number relation is automatic and will accept ID integers children = ChildSerializer() # this one will not class Meta: model = Parent 

La mejor solución aquí es utilizar dos campos diferentes: uno para leer y otro para escribir. Sin hacer un trabajo pesado , es difícil obtener lo que buscas en un solo campo .

El campo de solo lectura sería su serializador nested (en este caso, ChildSerializer ) y le permitirá obtener la misma representación anidada que espera. La mayoría de las personas lo definen como un child , porque ya tienen su front-end escrito para este punto y cambiarlo causaría problemas.

El campo de solo escritura sería un PrimaryKeyRelatedField , que es lo que normalmente usaría para asignar objetos en función de su clave principal. Esto no tiene que ser solo de escritura, especialmente si está intentando buscar simetría entre lo que se recibe y lo que se envía, pero parece que eso podría ser lo mejor para usted. Este campo debe tener un source establecido en el campo de clave foránea ( child en este ejemplo) para que lo asigne correctamente en la creación y actualización.


Esto se ha mencionado en el grupo de discusión varias veces, y creo que sigue siendo la mejor solución. Gracias a Sven Maurer por señalarlo .

Este es un ejemplo de lo que habla la respuesta de Kevin, si desea adoptar ese enfoque y utilizar 2 campos separados.

En tus modelos.py …

 class Child(models.Model): name = CharField(max_length=20) class Parent(models.Model): name = CharField(max_length=20) phone_number = models.ForeignKey(PhoneNumber) child = models.ForeignKey(Child) 

entonces serializers.py …

 class ChildSerializer(ModelSerializer): class Meta: model = Child class ParentSerializer(ModelSerializer): # if child is required child = ChildSerializer(read_only=True) # if child is a required field and you want write to child properties through parent # child = ChildSerializer(required=False) # otherwise the following should work (untested) # child = ChildSerializer() child_id = serializers.PrimaryKeyRelatedField( queryset=Child.objects.all(), source='child', write_only=True) class Meta: model = Parent 

Establecer source=child permite que child_id actúe como child de forma predeterminada si no se anulara (nuestro comportamiento deseado). write_only=True hace que child_id esté disponible para escribir, pero evita que aparezca en la respuesta ya que el ID ya aparece en ChildSerializer

Usar dos campos diferentes estaría bien (como mencionaron @Kevin Brown y @joslarson ), pero creo que no es perfecto (para mí). Debido a que obtener datos de una clave ( child ) y enviar datos a otra clave ( child_id ) puede ser un poco ambiguo para los desarrolladores front-end . (sin ofender en absoluto)

Por lo tanto, lo que sugiero aquí es anular el método to_representation() de ParentSerializer que hará el trabajo.

 def to_representation(self, instance): response = super().to_representation(instance) response['child'] = ChildSerializer(instance.child).data return response 


Representación completa de Serializador

 class ChildSerializer(ModelSerializer): class Meta: model = Child fields = '__all__' class ParentSerializer(ModelSerializer): class Meta: model = Parent fields = '__all__' def to_representation(self, instance): response = super().to_representation(instance) response['child'] = ChildSerializer(instance.child).data return response 

¿Ventaja de este método?

Al usar este método, no necesitamos dos campos separados para la creación y la lectura. Aquí tanto la creación como la lectura se pueden hacer usando child tecla child .

Carga útil de muestra para crear instancia parent

 { "name": "TestPOSTMAN_name", "phone_number": 1, "child": 1 } 

Captura de pantalla
Captura de pantalla POSTMAN

Hay una manera de sustituir un campo en la operación de crear / actualizar:

 class ChildSerializer(ModelSerializer): class Meta: model = Child class ParentSerializer(ModelSerializer): child = ChildSerializer() # called on create/update operations def to_internal_value(self, data): self.fields['child'] = serializers.PrimaryKeyRelatedField( queryset=Child.objects.all()) return super(ParentSerializer, self).to_internal_value(data) class Meta: model = Parent 

Así es como he resuelto este problema.

serializers.py

 class ChildSerializer(ModelSerializer): def to_internal_value(self, data): if data.get('id'): return get_object_or_404(Child.objects.all(), pk=data.get('id')) return super(ChildSerializer, self).to_internal_value(data) 

Simplemente pasará su serializador hijo nested al igual que lo obtiene del serializador, es decir, child como un json / diccionario. en to_internal_value creamos una instancia del objeto secundario si tiene una ID válida para que DRF pueda seguir trabajando con el objeto.

Algunas personas aquí han colocado una forma de mantener un campo pero aún así pueden obtener los detalles al recuperar el objeto y crearlo solo con la ID. Hice un poco más de implementación genérica si la gente está interesada:

Primero de las pruebas:

 from rest_framework.relations import PrimaryKeyRelatedField from django.test import TestCase from .serializers import ModelRepresentationPrimaryKeyRelatedField, ProductSerializer from .factories import SomethingElseFactory from .models import SomethingElse class TestModelRepresentationPrimaryKeyRelatedField(TestCase): def setUp(self): self.serializer = ModelRepresentationPrimaryKeyRelatedField( model_serializer_class=SomethingElseSerializer, queryset=SomethingElse.objects.all(), ) def test_inherits_from_primary_key_related_field(self): assert issubclass(ModelRepresentationPrimaryKeyRelatedField, PrimaryKeyRelatedField) def test_use_pk_only_optimization_returns_false(self): self.assertFalse(self.serializer.use_pk_only_optimization()) def test_to_representation_returns_serialized_object(self): obj = SomethingElseFactory() ret = self.serializer.to_representation(obj) self.assertEqual(ret, SomethingElseSerializer(instance=obj).data) 

Entonces la clase en sí:

 from rest_framework.relations import PrimaryKeyRelatedField class ModelRepresentationPrimaryKeyRelatedField(PrimaryKeyRelatedField): def __init__(self, **kwargs): self.model_serializer_class = kwargs.pop('model_serializer_class') super().__init__(**kwargs) def use_pk_only_optimization(self): return False def to_representation(self, value): return self.model_serializer_class(instance=value).data 

El uso es así, si tienes un serializador en algún lugar:

 class YourSerializer(ModelSerializer): something_else = ModelRepresentationPrimaryKeyRelatedField(queryset=SomethingElse.objects.all(), model_serializer_class=SomethingElseSerializer) 

Esto le permitirá crear un objeto con una clave externa aún solo con el PK, pero devolverá el modelo nested serializado completo cuando recupere el objeto que creó (o cuando realmente).

Creo que el enfoque descrito por Kevin probablemente sería la mejor solución, pero nunca podría hacerlo funcionar. El DRF siguió generando errores cuando tenía un serializador nested y un campo de clave principal configurado. Eliminar uno u otro funcionaría, pero obviamente no me dio el resultado que necesitaba. Lo mejor que se me ocurrió es crear dos serializadores diferentes para leer y escribir, Al igual que …

serializers.py:

 class ChildSerializer(serializers.ModelSerializer): class Meta: model = Child class ParentSerializer(serializers.ModelSerializer): class Meta: abstract = True model = Parent fields = ('id', 'child', 'foo', 'bar', 'etc') class ParentReadSerializer(ParentSerializer): child = ChildSerializer() 

vistas.py

 class ParentViewSet(viewsets.ModelViewSet): serializer_class = ParentSerializer queryset = Parent.objects.all() def get_serializer_class(self): if self.request.method == 'GET': return ParentReadSerializer else: return self.serializer_class 

¡Hay un paquete para eso! Echa un vistazo a PresentablePrimaryKeyRelatedField en el paquete Drf Extra Fields.

https://github.com/Hipo/drf-extra-fields