python - primarykeyrelatedfield - serializer django rest framework
DRF: asignaciĆ³n de clave externa simple con serializadores anidados? (8)
Con Django REST Framework, un ModelSerializer estándar permitirá asignar o cambiar las relaciones del modelo ForeignKey PUBLICANDO un ID como un entero.
¿Cuál es la forma más sencilla de obtener este comportamiento de un serializador anidado?
Tenga en cuenta que solo estoy hablando de asignar objetos de base de datos existentes, no creación anidada.
En el pasado, he eliminado estos campos con campos de ''identificación'' adicionales en el serializador y con métodos personalizados de
create
y
update
, pero este es un problema aparentemente tan simple y frecuente para mí que tengo curiosidad por saber 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
¡Hay un paquete para eso! Consulte PresentablePrimaryKeyRelatedField en el paquete Drf Extra Fields.
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 una implementación un poco más 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 misma:
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 tiene un serializador en alguna parte:
class YourSerializer(ModelSerializer):
something_else = ModelRepresentationPrimaryKeyRelatedField(queryset=SomethingElse.objects.all(), model_serializer_class=SomethingElseSerializer)
Esto le permitirá crear un objeto con una clave foránea aún solo con la PK, pero devolverá el modelo anidado serializado completo al recuperar el objeto que creó (o cuando sea realmente).
Aquí hay un ejemplo de lo que está hablando 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 hijo por defecto si no se anula (nuestro comportamiento deseado).
write_only=True
hace que
child_id
esté disponible para escribir, pero evita que aparezca en la respuesta ya que la identificación ya aparece en
ChildSerializer
.
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 anidado tal como lo obtiene del serializador, es decir, hijo como un json / diccionario.
en
to_internal_value
instanciamos el objeto hijo si tiene una ID válida para que DRF pueda seguir trabajando con el objeto.
Creo que el enfoque esbozado por Kevin probablemente sería la mejor solución, pero nunca pude lograr que funcione. DRF seguía arrojando errores cuando tenía tanto un serializador anidado como un conjunto de campos de clave primaria. Eliminar uno u otro funcionaría, pero obviamente no me dio el resultado que necesitaba. Lo mejor que se me ocurre es crear dos serializadores diferentes para leer y escribir, así ...
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()
views.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 una manera de sustituir un campo en la operación de creación / actualización:
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
La mejor solución aquí es usar dos campos diferentes: uno para leer y el otro para escribir. Sin hacer algo de trabajo pesado , es difícil obtener lo que está buscando en un solo campo .
El campo de solo lectura sería su serializador anidado (
ChildSerializer
en este caso) y le permitirá obtener la misma representación anidada que espera.
La mayoría de las personas definen esto como solo un
child
, porque ya tienen su front-end escrito en 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á tratando de obtener simetría entre lo que se recibe y lo que se envía, pero parece que eso podría ser mejor para usted.
Este campo debe tener
una
source
establecida en el campo de clave externa (
child
en este ejemplo) para que se asigne correctamente en la creación y actualización.
Esto ha sido mencionado en el grupo de discusión varias veces, y creo que esta sigue siendo la mejor solución. Gracias a Sven Maurer por señalarlo .
Usar dos campos diferentes estaría
bien
(como mencionaron
y
), pero creo que no es
perfecto
(para mí).
Porque 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)
Entonces, lo que sugiero aquí es que anule 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 utilizando
child
clave
child
.
Carga útil de muestra para crear
parent
instancia
parent
{
"name": "TestPOSTMAN_name",
"phone_number": 1,
"child": 1
}