Django-rest-framework: Las llamadas PUT no "reemplazan completamente el estado del recurso de destino"

Creado en 30 jun. 2016  ·  68Comentarios  ·  Fuente: encode/django-rest-framework

EDITAR: para conocer el estado actual del problema, vaya a https://github.com/encode/django-rest-framework/issues/4231#issuecomment -332935943

===

Tengo un problema al implementar una biblioteca Optimistic Concurrency en una aplicación que usa DRF para interactuar con la base de datos. Estoy tratando de:

  • Confirme que el comportamiento que estoy viendo es atribuible a DRF
  • Confirme que este es el comportamiento previsto.
  • Determine si hay alguna forma práctica de superar este comportamiento.

Recientemente agregué concurrencia optimista a mi aplicación Django. Para ahorrarte la búsqueda Wiki:

  • Cada modelo tiene un campo de versión.
  • Cuando un editor edita un objeto, obtiene la versión del objeto que está editando.
  • Cuando el editor guarda el objeto, el número de versión incluido se compara con la base de datos
  • Si las versiones coinciden, el editor actualizó el documento más reciente y se guarda.
  • Si las versiones no coinciden, asumimos que se envió una edición "conflictiva" entre el momento en que se cargó el editor y se guardó, por lo que rechazamos la edición.
  • Si falta la versión, no podemos hacer pruebas y debemos rechazar la edición.

Tenía una interfaz de usuario heredada hablando a través de DRF. La interfaz de usuario heredada no manejaba los números de versión. Esperaba que esto causara errores de concurrencia, pero no fue así. Si entiendo la discusión en #3648 correctamente:

  • DRF fusiona el PUT con el registro existente. Esto hace que una ID de versión faltante se complete con la ID de la base de datos actual
  • Dado que esto siempre proporciona una coincidencia, la omisión de esta variable siempre romperá un sistema de concurrencia optimista que se comunica a través de DRF
  • ~No hay opciones fáciles (como hacer que el campo sea "obligatorio") para garantizar que los datos se envíen cada vez.~ (editar: puede solucionar el problema haciéndolo obligatorio como se demuestra en este comentario )

pasos para reproducir

  1. Configurar un campo de simultaneidad optimista en un modelo
  2. Cree una nueva instancia y actualícela varias veces (para asegurarse de que ya no tiene un número de versión predeterminado)
  3. Envíe una actualización (PUT) a través de DRF excluyendo la ID de la versión

    Comportamiento esperado

El ID de la versión que falta no debe coincidir con la base de datos y causar un problema de simultaneidad.

Comportamiento real

DRF completa el ID de versión faltante con el ID actual para que pase la verificación de simultaneidad.

Enhancement

Todos 68 comentarios

De acuerdo, no puedo prometer que podré revisar este ticket bastante en profundidad de inmediato, ya que la próxima versión 3.4 tiene prioridad. Pero gracias por un tema tan detallado y bien pensado. Lo más probable es que esto se mire en la escala de semanas, no de días o meses. Si hace algún progreso, tiene más ideas, actualice el ticket y manténganos informados.

está bien. Estoy bastante seguro de que mi problema es la combinación de dos factores:

  1. DRF no requiere el campo en PUT (aunque es obligatorio en el modelo) porque tiene un valor predeterminado (versión = 0)
  2. DRF fusiona los campos PUT con el objeto actual (sin inyectar el valor predeterminado)

Como resultado, DRF usa el valor actual (base de datos) y rompe el control de concurrencia. La segunda mitad del problema está relacionada con la discusión en el n.º 3648 (también citada anteriormente) y hay una discusión (anterior a 3.x) en el n.º 1445 que todavía parece ser relevante.

Espero que un caso concreto (y cada vez más común) en el que el comportamiento predeterminado sea perverso sea suficiente para reabrir la discusión sobre el comportamiento "ideal" de un ModelSerializer. Obviamente, solo tengo una pulgada de profundidad en DRF, pero mi intuición es que el siguiente comportamiento es apropiado para un campo obligatorio y un PUT:

  • Al usar un serializador no parcial, debemos recibir el valor, usar el valor predeterminado o (si no hay ningún valor predeterminado disponible) generar un error de validación. La validación de todo el modelo debe aplicarse solo a las entradas/valores predeterminados.
  • Cuando usamos un serializador parcial, debemos recibir el valor o recurrir a los valores actuales. La validación de todo el modelo debe aplicarse a esos datos combinados.
  • Creo que el serializador "no parcial" actual es realmente casi parcial:

    • No es parcial para los campos que son obligatorios y no tienen valores predeterminados.

    • Es parcial para los campos que son obligatorios y tienen un valor predeterminado (ya que no se usa el valor predeterminado)

    • Es parcial para campos que no son obligatorios

No podemos cambiar la viñeta (1) anterior o los valores predeterminados se vuelven inútiles (requerimos la entrada aunque conocemos el valor predeterminado). Eso significa que tenemos que solucionar el problema cambiando el número 2 anterior. Estoy de acuerdo con su argumento en el n.° 2683 de que:

Los valores predeterminados del modelo son los valores predeterminados del modelo. El serializador debe omitir el valor y transferir la responsabilidad al Model.object.create() para manejar esto.

Para ser consistente con esa separación de preocupaciones, la actualización debe crear una nueva instancia (delegando todos los valores predeterminados al modelo) y aplicar los valores enviados a esa nueva instancia. Esto da como resultado el comportamiento solicitado en #3648.

Tratar de describir la ruta de migración ayuda a resaltar cuán extraño es el comportamiento actual. El objetivo final es

  1. Arreglar el ModelSerializer,
  2. Agregue una bandera para este estado cuasi-parcial, y
  3. Haga que esa bandera sea la predeterminada (para compatibilidad con versiones anteriores)

¿Cómo se llama esa bandera? El serializador de modelo actual es en realidad un serializador parcial que (algo arbitrariamente) requiere campos que cumplan la condición required==True and default==None . No podemos usar explícitamente el indicador partial sin romper la compatibilidad con versiones anteriores, por lo que necesitamos un indicador nuevo (con suerte, temporal). Me quedo con quasi_partial , pero mi incapacidad para expresar el requisito arbitrario required==True and default==None es la razón por la que me resulta tan claro que este comportamiento debe quedar obsoleto con urgencia.

Puede agregar extra_kwargs en el Meta del serializador, haciendo que version sea ​​un campo obligatorio.

class ConcurrentModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = ConcurrentModel
        extra_kwargs = {'version': {'required': True}}

Gracias @anoopmalev. Eso me mantendrá en la rama de producción.

Después de "dormir en él" me doy cuenta de que hay una arruga extra. Todo lo que dije debería aplicarse a los campos del serializador. Si un campo no está incluido en el serializador, no debe modificarse. De esta forma, todos los serializadores son (y deberían ser) parciales para los campos no incluidos. Esto es un poco más complicado que mi "crear una nueva instancia" anterior.

Creo que este tema debe reducirse a una propuesta más limitada para poder avanzar.
Parece demasiado amplio para ser procesable en su estado actual.
Por ahora estoy cerrando esto: si alguien puede reducirlo a una declaración concisa y procesable del comportamiento deseado, entonces podemos reconsiderarlo. Hasta entonces, creo que es simplemente demasiado amplio.

Aquí hay una propuesta concisa... para un serializador no parcial:

  1. Para cualquier campo que no aparezca en el serializador (implícita o explícitamente) o marcado como de solo lectura, conserve el valor existente
  2. Para todos los demás campos, utilice la primera opción disponible:

    1. Rellenar con el valor enviado

    2. Complete con un valor predeterminado, incluido un valor implícito en blank y/o null

    3. Levantar una excepción

Para mayor claridad, la validación se ejecuta en el producto final de este proceso.

Es decir, desea establecer required=True en cualquier campo de serializador que no tenga un modelo predeterminado, para actualizaciones.

¿Tengo eso correcto?

Sí (y más). Así es como entiendo la distinción partial (todos los campos son opcionales) frente a non-partial (todos los campos son obligatorios). La única vez que un serializador non-partial no requiere un campo es la presencia de un valor predeterminado (definido de manera estricta o amplia), ya que el serializador puede usar ese valor predeterminado si no se proporciona ningún valor._

La sección en cursiva es lo que DRF no está haciendo actualmente y el cambio más importante en mi propuesta. La implementación actual simplemente omite el campo.

Tuve una segunda propuesta mezclada, pero en realidad es una cuestión aparte de cuán generoso quieres ser con la idea de un "predeterminado". El comportamiento actual es "estricto" en el sentido de que solo default se trata como tal. Si _realmente_ quisiera reducir la cantidad de datos requeridos, también podría hacer que los campos blank=True sean opcionales... asumiendo que un valor ausente es un valor en blanco.

@claytondaley estoy usando OOL con DRF desde 2x de esta manera:

class VersionModelSerializer(serializers.ModelSerializer, BaseSerializer):
    _initial_version = 0

    _version = VersionField()

    def __init__(self, *args, **kwargs):
        super(VersionModelSerializer, self).__init__(*args, **kwargs)

        # version field should not be required if there is no object
        if self.instance is None and '_version' in self.fields and\
                getattr(self, 'parent', None) is None:
            self.fields['_version'].read_only = True
            self.fields['_version'].required = False

        # version field is required while updating instance
        if self.instance is not None and '_version' in self.fields:
            self.fields['_version'].required = True

        if self.instance is not None and hasattr(self.instance, '_version'):
            self._initial_version = self.instance._version

    def validate__version(self, value):
        if self.instance is not None:
            if not value and not isinstance(value, int):
                raise serializers.ValidationError(_(u"This field is required"))

        return value
   # more code & helpers

funciona muy bien con todo tipo de lógica comercial y nunca causó ningún problema.

¿Se dejó cerrado por accidente? Respondí a la pregunta específica y no escuché una razón por la que estaba mal con la propuesta.

@claytondaley ¿por qué OOL debería ser parte de DRF? Verifique mi código: funciona solo encuéntrelo en una aplicación grande (1400 pruebas). VersionField es solo un IntegerField .

Ha codificado el OOL en el serializador. Este es el lugar equivocado para hacerlo porque tienes una condición de carrera. Todas las actualizaciones paralelas (con la misma versión anterior) pasarían al serializador... pero solo una ganaría en la acción de guardar.

Estoy usando django-concurrency que pone la lógica OOL en la acción de guardar (donde pertenece). Básicamente UPDATE... WHERE version = submitted_version . Esto es atómico por lo que no hay condición de carrera. Sin embargo, expone una falla en la lógica de serialización:

  • Si se establece el valor predeterminado en un campo del modelo, DRF establece required=False . La idea (válida) es que DRF puede usar ese valor predeterminado si no se envía ningún valor.
  • Sin embargo, si falta ese campo, DRF no usa el valor predeterminado. En su lugar, combina los datos enviados con la versión actual del objeto.

Cuando no requerimos el campo, lo hacemos porque tenemos un valor predeterminado para usar. DRF no cumple con ese contrato porque no usa el valor predeterminado... usa el valor existente.

El problema subyacente se discutió antes, pero no tenían un caso concreto y agradable. OOL es ese caso ideal. El valor existente de un campo de versión siempre pasa OOL, por lo que puede omitir todo el sistema OOL omitiendo la versión. Ese (obviamente) no es el comportamiento deseado de un sistema OOL.

@claytondaley

Ha codificado el OOL en el serializador.

¿Hice? ¿Ha encontrado alguna lógica OOL en mi serializador además del requisito de campo?

Este es el lugar equivocado para hacerlo porque tienes una condición de carrera.

Sry, simplemente no puedo ver dónde está la condición de carrera aquí.

Estoy usando django-concurrency que pone la lógica OOL en la acción de guardar (donde pertenece).

También estoy usando django-concurrency :) Pero ese es el nivel de modelo, no el serializador. En el nivel del serializador solo necesita:

  • asegúrese de que el campo _version siempre sea obligatorio (cuando debería serlo)
  • asegúrese de que su serializador sepa cómo manejar los errores OOL (esta parte la he omitido)
  • asegúrese de que su apiview sepa cómo manejar los errores OOL y genere HTTP 409 con un posible contexto diferencial

en realidad, no estoy usando django-concurrency debido a un problema que el autor marcó como "no solucionará": omite OOL cuando se usa obj.save(update_fields=['one', 'two', 'tree']) , lo que encontré como una mala práctica, así que bifurqué el paquete.

aquí está el método save faltante del serializador que mencioné anteriormente. eso debería resolver todos tus problemas:

    def save(self, **kwargs):
        try:
            self.instance = super(VersionModelSerializer, self).save(**kwargs)
            return self.instance
        except VersionException:
            # Use select_for_update so we have some level of guarantee
            # that object won't be modified at least here at the same time
            # (but it may be modified somewhere else, where select_for_update
            # is not used!)
            with transaction.atomic():
                db_instance = self.instance.__class__.objects.\
                    select_for_update().get(pk=self.instance.pk)
                diff = self._get_serializer_diff(db_instance)

                # re-raise exception, so api client will receive friendly
                # printed diff with writable fields of current serializer
                if diff:
                    raise VersionException(diff)

                # otherwise re-try saving using db_instance
                self.instance = db_instance
                if self.is_valid():
                    return super(VersionModelSerializer, self).save(**kwargs)
                else:
                    # there are errors that could not be displayed to a user
                    # so api client should refresh & retry by itself
                    raise VersionException

        # instance.save() was interrupted by application error
        except ApplicationException as logic_exc:
            if self._initial_version != self.instance._version:
                raise VersionException

            raise logic_exc

Lo siento. No leí tu código para averiguar qué estabas haciendo. Vi un serializador. Obviamente, puede solucionar el problema pirateando el serializador, pero no debería tener que hacerlo... porque la falla en la lógica DRF se sostiene por sí sola. Solo estoy usando OOL para aclarar el punto.

Y debería probar ese código con la última versión de django-concurrency (usando IGNORE_DEFAULT=False ). django-concurrency también ignoraba los valores predeterminados, pero envié un parche. Hubo un caso de esquina extraño que tuve que buscar para que funcionara para casos normales.

Creo que se llama extender la funcionalidad predeterminada, no realmente piratear. Creo que el mejor lugar para este tipo de soporte de funciones es el paquete django-concurrency .

Volví a leer toda la discusión del problema y encontré que su propuesta es demasiado amplia y fallaría en muchos lugares (debido al uso mágico de valores predeterminados de diferentes fuentes en diferentes condiciones). DRF 3.x ahora es mucho más fácil y predecible que 2.x, sigamos así :)

No puede arreglar esto en la capa del modelo porque está roto en el serializador (antes de que llegue al modelo). Deje a un lado OOL... ¿por qué no requerimos un campo si se establece default ?

Un serializador no parcial "requiere" todos los campos (fundamentalmente) y, sin embargo, dejamos pasar este. ¿Es un error? ¿O tenemos una razón lógica?

como puede ver en mi ejemplo de código, el campo _version siempre se requiere correctamente en todos los casos posibles.

por cierto, resultó que tomé prestado el código de modelo lvl de https://github.com/gavinwahl/django-optimistic-lock y no de django-concurrency que es demasiado complejo casi sin motivo.

... entonces el error es "los serializadores no parciales configuraron incorrectamente algunos campos como no requeridos". Esa es la alternativa. Porque ese es el compromiso (implícito) que hace un serializador no parcial.

Puedo citarlo :

De forma predeterminada, a los serializadores se les deben pasar valores para todos los campos obligatorios o generarán errores de validación.

Esto no dice nada sobre requerido (excepto cuando se proporciona un valor predeterminado).

(y entiendo que estoy hablando de dos niveles diferentes, pero ModelSerializer no debería dejar de requerir campos si no va a asumir la responsabilidad de esa decisión)

Creo que he perdido tu punto..

(y entiendo que estoy hablando de dos niveles diferentes, pero ModelSerializer no debería dejar de requerir campos si no va a asumir la responsabilidad de esa decisión)

¿Qué está mal con eso?

OK, déjame probar un ángulo diferente.

  • Supongamos que tengo un serializador de modelo no parcial (editar: todos los valores predeterminados) que cubre todos los campos de mi modelo.

Si una CREACIÓN o ACTUALIZACIÓN con los mismos datos alguna vez produce un objeto diferente (menos la ID)

¿Puede describir sus ideas usando un modelo y serializador realmente simple y pocas líneas que muestren un comportamiento fallido/esperado?

Prepararé algo mañana ya que se está haciendo tarde aquí... pero cuanto más profundice, más sentido tiene #3648 para un serializador no parcial. Mientras tanto, ¿por qué un ModelSerializer no requiere todos los campos del modelo? Tal vez tu razón sea diferente a la mía.

ModelSerializer inspecciona el modelo acotado y decide si debería ser necesario, ¿no es así?

No me refiero mecánicamente cómo . La suposición básica para un Serializador no parcial es requerir todo (citado arriba). Si get_field_kwargs se va a desviar de esta suposición (específicamente, aquí ), debe haber una buena razón. ¿Cuál es esa razón?

Mi respuesta preferida es la que sigo dando, "porque puede usar ese valor predeterminado si no se envía ningún valor" (pero entonces DRF tiene que usar el valor predeterminado). ¿Hay otra respuesta que me falta? ¿Una razón por la cual los campos con valores predeterminados no deberían ser obligatorios?

Obviamente, prefiero la solución "completa". Sin embargo, admitiré que hay una segunda respuesta. Podríamos requerir estos campos por defecto. Eso elimina el caso especial (actualmente arbitrario). Simplifica/reduce el código. Es internamente consistente. Se dirige a mi preocupación.

Básicamente, hace que el serializador no parcial sea realmente no parcial.

Ahora al menos sé lo que quieres decir. ¿Ha comprobado cuál es el comportamiento de ModelForm en tal caso? (No puedo hacer esto yo mismo en el móvil)

Django docs dice que 'en blanco' controla si el campo es obligatorio o no. Le sugiero que abra un ticket por separado para este problema, ya que contiene muchos comentarios no relacionados. En mi opinión, modelserializer podría funcionar como modelform: se requieren controles de opción en blanco, 'nulo' indica si Ninguno es una entrada aceptable y 'predeterminado' no tiene efecto en esa lógica.

Estoy dispuesto a abrir un segundo ticket, pero me preocupa que el espacio en blanco requiera un código similar. Del grupo de discusión Django :

si tomamos un formulario de modelo existente y una plantilla que funciona, agregamos un campo de carácter opcional al modelo pero fallamos al agregar un campo correspondiente a la plantilla HTML (por ejemplo, error humano, olvidó una plantilla, no le dijo al autor de la plantilla que hiciera un cambio, no me di cuenta de que era necesario realizar un cambio en una plantilla), cuando se envía ese formulario, Django asumirá que el usuario ha proporcionado un valor de cadena vacío para el campo faltante y lo guardará en el modelo, borrando cualquier valor existente .

Para ser coherentes, tendríamos la obligación de cumplir con la segunda mitad del contrato, dejando en blanco el valor ausente. Esto es un poco menos problemático porque se puede llenar un espacio en blanco sin referencia a un modelo, pero es muy similar (y, creo, consistente con #3648).

@tomchristie , ¿puede darnos una breve información sobre esto? ¿Por qué el estado required depende de la propiedad del campo modelo defaults ?

¿Por qué el estado requerido depende de la propiedad predeterminada del campo del modelo?

Simplemente esto: si un campo de modelo tiene un valor predeterminado, puede omitir proporcionarlo como entrada.

En realidad estoy de acuerdo con este comportamiento. ModelForm a pesar de que el código está haciendo lo mismo (el html generado proporcionará valores predeterminados). Si DRF tuviera una lógica diferente, nunca se aplicará 'predeterminado'. He terminado con este problema.

@pySilver en realidad, aquí está el comportamiento de ModelForm:

# models.py

from django.db import models

class MyModel(models.Model):
    no_default = models.CharField(max_length=100)
    has_default = models.CharField(max_length=100, default="iAmTheDefault")

Para mayor claridad, las cosas aún se denominan "parciales" porque la _actualización_ es parcial. También estaba probando una actualización completa ("completa"), pero el código no era necesario para mostrar el comportamiento:

# in manage.py shell
>>> from django import forms
>>> from django.conf import settings
>>> from form_serializer.models import MyModel
>>>
>>> class MyModelForm(forms.ModelForm):
...     class Meta:
...         model = MyModel
...         fields = ['no_default', 'has_default']
...
>>>
>>> partial = MyModel.objects.create()
>>> partial.id = 2
>>> partial.no_default = "Must replace me"
>>> partial.has_default = "I should be replaced"
>>> partial.save()
>>>
>>>
>>> POST_PARTIAL = {
...     "id": 2,
...     "no_default": "must change me",
... }
>>>
>>>
>>> form_partial = MyModelForm(POST_PARTIAL)
>>> form_partial.is_valid()
False
>>> form_partial._errors
{'has_default': [u'This field is required.']}

ModelForm requiere esta entrada aunque tiene un valor predeterminado. Este es uno de los dos comportamientos internamente consistentes.

¿Por qué el estado requerido depende de la propiedad predeterminada del campo del modelo?

Simplemente esto: si un campo de modelo tiene un valor predeterminado, puede omitir proporcionarlo como entrada.

@tomchristie está de acuerdo en principio. Pero, ¿cuál es el comportamiento esperado?

  • Al crear, obtengo el valor predeterminado (trivial, todos están de acuerdo en que esto es correcto)
  • En la actualización, ¿qué debo obtener?

Me parece que también debería obtener el valor predeterminado en la actualización. No veo por qué un serializador no parcial debería comportarse de manera diferente en los dos casos. No parcial significa que estoy enviando el registro "completo". Por lo tanto, el registro completo debe ser reemplazado.

Espero que el valor no cambie si no se proporciona en la actualización. Veo el punto, pero sobrescribir de forma transparente con el valor predeterminado sería contrario a la intuición desde mi punto de vista.

(En todo caso, creo que probablemente sería mejor que todas las actualizaciones fueran semánticas parciales para todos los campos: PUT seguiría siendo idempotente, que es el aspecto importante, aunque posiblemente sea incómodo cambiarlo dado el comportamiento actual)

Ciertamente no comparto tus preferencias; Quiero que todas mis interfaces sean estrictas a menos que las haga deliberadamente de otra manera. Sin embargo, su distinción PARCIAL vs. NO PARCIAL ya proporciona (en teoría) lo que ambos queremos.

Creo que parcial se comporta exactamente como quieres:

  • Las ACTUALIZACIONES son 100% parciales
  • Los CREATE (supongo) son parciales con respecto a default y blank (excepciones lógicas). En todos los demás casos, las restricciones del modelo/base de datos se vinculan.

Solo estoy tratando de obtener consistencia en el serializador no parcial. Si elimina el caso especial para default , sus serializadores no parciales existentes se convierten en el serializador estricto que quiero. También alcanzan la paridad con ModelForm.

Me doy cuenta de que esto crea una pequeña discontinuidad dentro del proyecto, pero esta no es la primera vez que alguien hace un cambio como este. Agregue un indicador "heredado" predeterminado al comportamiento actual, agregue una advertencia (que el comportamiento predeterminado cambiará) y cambie el valor predeterminado en una versión principal posterior.

Más importante aún, si desea que sus serializadores sean los nuevos de facto para Django, terminará haciendo este cambio de todos modos. La cantidad de personas que se convertirán de ModelForm superará con creces la base de usuarios existente y esperarán al menos este cambio.

Insertando mis dos centavos:
Me inclino a estar de acuerdo con @claytondaley. PUT es un reemplazo de recurso idempotente, PATCH es una actualización del recurso existente. Tome el siguiente ejemplo:

class Profiles(models.Model):
    username = models.CharField()
    role = models.CharField(default='member', choices=(
        ('member', 'Member'), 
        ('moderator', 'Moderator'),
        ('admin', 'Admin'), 
    ))

Los nuevos perfiles tienen sensatamente el rol de miembro predeterminado. Tomemos las siguientes solicitudes:

POST /profiles username=moe
PUT /profiles/1 username=curly
PATCH /profiles/1 username=larry&role=admin
PUT /profiles/1 username=curly

Tal como está actualmente, después del primer PUT, los datos del perfil contendrían {'username': 'curly', 'role': 'member'} . Después del segundo PUT, tendrías {'username': 'curly', 'role': 'admin'} . ¿No rompe esto la idempotencia? (No estoy del todo seguro, lo pregunto legítimamente)

Editar:
Creo que todos están en la misma página sobre la semántica de PATCH.

Después del segundo PUT, tendrías {'username': 'curly', 'role': 'admin'}

Personalmente, me sorprendería si el rol volviera al valor predeterminado (aunque veo el motivo de esta discusión sobre el objeto replace , nunca he tenido ningún problema real con él todavía)

nunca he tenido ningún problema del mundo real con él todavía

Lo mismo aquí, pero hasta ahora nuestros proyectos se han basado en PATCH :)
Dicho esto, el caso de uso del OP con el control de versiones del modelo para manejar la concurrencia tiene sentido para mí. Esperaría que PUT use el valor predeterminado (si se omite el valor), generando la excepción de concurrencia.

Permítanme comenzar reconociendo que un serializador no necesariamente debe seguir el RFCful RFC. Sin embargo, al menos deberían ofrecer modos que _son_ compatibles, especialmente en un paquete que ofrece soporte REST.

Mi argumento original era de principios básicos, pero el RFC (sección 4.3.4) dice específicamente (énfasis agregado):

La diferencia fundamental entre los métodos POST y PUT se destaca por la diferente intención de la representación adjunta. El recurso de destino en una solicitud POST está diseñado para manejar la representación adjunta de acuerdo con la semántica propia del recurso, mientras que la representación adjunta en una solicitud PUT se define como el reemplazo del estado del recurso de destino.
...
Un servidor de origen que permite PUT en un recurso de destino dado DEBE enviar una respuesta 400 (Solicitud incorrecta) a una solicitud PUT que contiene un campo de encabezado de Rango de contenido (Sección 4.2 de [RFC7233]), ya que es probable que la carga útil sea contenido parcial que se ha PUTADO erróneamente como una representación completa . Las actualizaciones de contenido parciales son posibles al apuntar a un recurso identificado por separado con un estado que se superpone a una parte del recurso más grande, o al usar un método diferente que se ha definido específicamente para actualizaciones parciales (por ejemplo, el método PATCH definido en [RFC5789])

Por lo tanto, un PUT nunca debe ser parcial (consulte también aquí ). Sin embargo, la sección sobre PUT también aclara:

El método PUT solicita que el estado del recurso de destino se cree o se reemplace con el estado definido por la representación incluida en la carga útil del mensaje de solicitud. Un PUT exitoso de una representación determinada sugeriría que un GET posterior en ese mismo recurso de destino dará como resultado que se envíe una representación equivalente en una respuesta 200 (OK).

El punto sobre el GET (aunque no es obligatorio) argumenta a favor de mi solución de "compromiso". Si bien inyectar espacios en blanco/predeterminados es conveniente, no proporcionaría este comportamiento. El clavo en el ataúd probablemente sea que esta solución minimiza la confusión ya que no faltarán campos para generar dudas.

Obviamente, PATCH es una opción específica para actualizaciones parciales, pero se describe como un "conjunto de instrucciones" en lugar de solo PUT parcial, por lo que siempre me inquieta un poco. La sección sobre POST (4.3.3) en realidad establece:

El método POST solicita que el recurso de destino procese la representación incluida en la solicitud de acuerdo con la semántica específica del recurso. Por ejemplo, POST se utiliza para las siguientes funciones (entre otras):

  • Proporcionar un bloque de datos, como los campos ingresados ​​en un formulario HTML, a un proceso de manejo de datos;

...

  • Agregar datos a las representaciones existentes de un recurso.

Creo que hay un argumento para usar POST para actualizaciones parciales ya que:

  • conceptualmente, modificar datos no es diferente de agregar
  • POST puede usar sus propias reglas, por lo que esas reglas podrían ser una actualización parcial
  • esta operación se puede distinguir fácilmente de CREATE por la presencia de una ID

Incluso si DRF no aspira a un cumplimiento total, necesitamos un serializador que sea compatible con la operación PUT de especificación (es decir, reemplazar todo el objeto). La respuesta más simple (y claramente menos confusa) es requerir todos los campos. También sugiere que PUT debería ser no parcial por defecto y que las actualizaciones parciales deberían usar una palabra clave diferente (PATCH o incluso POST).

Creo que acabo de encontrar mi primer problema PUT al migrar nuestra aplicación a drf3.4.x :)

<strong i="6">@cached_property</strong>
    def _writable_fields(self):
        return [
            field for field in self.fields.values()
            if (not field.read_only) or (field.default is not empty)
        ]

Esto hace que mi .validated_data contenga datos que no proporcioné en la solicitud PUT y que no proporcioné manualmente dentro del serializador. Los valores se recuperaron de default= en el nivel del serializador. Así que, básicamente, aunque tengo la intención de actualizar un campo en particular, también sobrescribo algunos de esos campos con valores predeterminados de la nada.

Feliz por mí, estoy usando ModelSerializer personalizado, por lo que puedo solucionar el problema fácilmente.

@pySilver No entiendo el contenido del último comentario.

@rpkilby "Tomemos las siguientes solicitudes... ¿Esto no rompe la idempotencia?"

No, cada solicitud PUT es idempotente en el sentido de que puede repetirse varias veces, lo que da como resultado el mismo estado. Eso no significa que si alguna otra parte del estado se ha modificado mientras tanto, de alguna manera se restablecerá.

Aquí hay algunas opciones diferentes para el comportamiento de PUT .

  • Los campos son obligatorios a menos que required=False o tengan default . (Existente)
  • Todos los campos son obligatorios. (Semántica más estricta y más estrechamente alineada de la actualización completa _pero_ incómoda porque en realidad es más estricta que la semántica de creación inicial para POST)
  • No se requieren campos (es decir, solo refleja el comportamiento de PATCH)

Claro que no hay una respuesta correcta _absoluta_, pero creo que tenemos el comportamiento más práctico tal como está actualmente.

Creo que algunos casos de uso pueden resultar problemáticos si hay un campo que no es necesario proporcionar para las solicitudes de POST , pero que posteriormente lo hace para las solicitudes de PUT . Además, PUT-as-create es en sí mismo una operación válida, por lo que, de nuevo, sería extraño si tuviera una semántica de "requerimiento" diferente a POST.

Si alguien quiere llevar esto adelante, sugeriría _fuertemente_ comenzar como un paquete de terceros, que implementa diferentes clases de serializador base. Luego podemos vincularnos a eso desde la documentación de los serializadores. Si el caso está bien hecho, entonces podemos considerar adaptar el comportamiento predeterminado en algún momento en el futuro.

@tomchristie lo que quiero decir:

Tengo un serializador con campo de solo lectura language y un modelo:

class Book(models.Model):
      title = models.CharField(max_length=100)
      language = models.ChoiceField(default='en', choices=(('pl', 'Polish'), ('en', 'English'))

class BookUpdateSerialzier(serializers.ModelSerializer):
      # language is readonly, I dont want to let users update that field using this serializer
      language = serializers.ChoiceField(default='en', choices=(('pl', 'Polish'), ('en', 'English'), read_only=True)
      class Meta:
          model = MyModel
          fields = ('title', 'language', )

book = Book(title="To be or 42", language="pl")
book.save()

s = BookUpdateSerialzier(book, data={'title': 'Foobar'}, partial=True)
s.is_valid()
assert 'language' in s.validated_data # !!! 
assert 'pl' == s.validated_data # AssertionError... here :(
  • No pasé language en la solicitud y no espero ver esto en los datos validados. Al pasar a update , sobrescribiría mi instancia con valores predeterminados a pesar de que el objeto ya tiene asignado algún valor no predeterminado.
  • Sería menos problemático si validated_data['language'] fuera book.language en ese caso.

@pySilver : sí, eso se resolvió en https://github.com/tomchristie/django-rest-framework/pull/4346 solo hoy.

Da la casualidad de que no necesita default= en el campo del serializador en el ejemplo que tiene, ya que tiene un valor predeterminado en ModelField .

@tomchristie ¿Al menos está de acuerdo en que el comportamiento PUT actual no es una especificación RFC? ¿Y que mis dos sugerencias (requerir todo o inyectar valores predeterminados) lo harían así?

@tomchristie ¡buenas noticias!

Da la casualidad de que no necesita default= en el campo del serializador en el ejemplo que tiene, ya que tiene un valor predeterminado en ModelField.

Sí, solo quería hacerlo súper explícito para la demostración.

Finalmente se está dando cuenta de que @tomchristie no está discutiendo a favor o en contra del comportamiento del serializador de forma aislada. Creo que sus objeciones se derivan (implícitamente) del requisito de que un único serializador admita todos los modos REST. Esto se muestra en sus quejas sobre cómo un serializador estricto afectará un POST. Dado que los modos REST son incompatibles, la solución actual es un serializador que no se especifica para ningún modo único.

Si esa es la verdadera raíz de la objeción, enfrentémosla de frente. ¿Cómo puede un solo serializador proporcionar un comportamiento de especificación para todos los modos REST? Mi respuesta improvisada es que PARCIAL frente a NO PARCIAL se implementa en el nivel incorrecto:

  • Disponemos de serializadores parciales y no parciales. Este enfoque significa que necesitamos varios serializadores para admitir el comportamiento de las especificaciones para todos los modos.
  • De hecho, necesitamos una validación parcial frente a una no parcial (o algo por el estilo). Los diferentes modos REST necesitan solicitar diferentes modos de validación del serializador.

Para proporcionar una separación de preocupaciones, un serializador no debe conocer el modo REST, por lo que no se puede implementar como un serializador de terceros (ni, sospecho, el serializador tiene acceso al modo). En su lugar, DRF debería pasar una información adicional al serializador (aproximadamente replace=True por PUT ). El serializador puede decidir cómo implementar esto (requerir todos los campos o inyectar los valores predeterminados).

Obviamente, esta es solo una propuesta aproximada, pero tal vez rompa el punto muerto.

Además, PUT-as-create es en sí mismo una operación válida, por lo que, de nuevo, sería extraño si tuviera una semántica de "requerimiento" diferente a POST.

Estoy de acuerdo en que puedes crear con un PUT, pero no estoy de acuerdo en que la semántica sea la misma. PUT funciona en un recurso específico:

El método PUT solicita que el estado del recurso de destino se cree o se reemplace con el estado definido por la representación incluida en la carga útil del mensaje de solicitud.

Creo, por lo tanto, que la semántica de creación en realidad difiere:

  • CORREOa /citizen/ espera que se genere un SSN (número de seguro social)
  • PONERa /citizen/<SSN> actualiza los datos para un SSN específico. Si no hay datos en ese SSN, se crea.

Debido a que el "id" debe incluirse en el URI de PUT, puede tratarlo según sea necesario. Por el contrario, el "id" es opcional en un POST.

Debido a que el "id" debe incluirse en el URI de PUT, puede tratarlo según sea necesario. Por el contrario, el "id" es opcional en un POST.

Por supuesto. Me refería específicamente al hecho de que el cambio propuesto de "hacer que PUT requiera estrictamente _todos_ los campos" significaría que PUT-as-create tendría un comportamiento diferente a POST-as-create wrt. si los campos son obligatorios o no.

Habiendo dicho eso, estoy llegando al valor de tener una opción de comportamiento PUT-is-strict.

(Haga cumplir que _todos_ los campos son estrictamente requeridos en este caso, haga cumplir que _no_ campos son requeridos en PATCH, y use el indicador required= para POST)

¿Cómo puede un solo serializador proporcionar un comportamiento de especificación para todos los modos REST?

Podemos diferenciar entre crear, actualizar y actualizar parcialmente dada la forma en que se instancia el serializador, por lo que no creo que sea un problema.

Ya ha señalado que puede create usando un PUT o POST . Tienen diferentes semánticas y diferentes requisitos, por lo que create debe ser independiente del modo REST. Creo que la distinción realmente sucede como parte de is_valid . Pedimos un modo de validación específico:

  • sin validación de presencia de campo (PATCH)
  • validación basada en required banderas (POST)
  • validación de presencia de campo estricta (PUT)

Al mantener la lógica específica de palabras clave fuera de las operaciones CRUD, también reducimos el acoplamiento entre el serializador y DRF. Si los modos de validación fueran configurables, serían completamente de propósito general (incluso si solo implementamos 3 casos específicos para nuestras 3 palabras clave).

Estás haciendo un buen trabajo discutiendo esta funcionalidad, ahí. :)

Los diferentes "modos de validación" al llamar a .is_valid() es un trastorno que no va a funcionar.

Podríamos considerar una contrapartida 'completa=Verdadera' de la unidad kwarg 'parcial=Verdadera' existente, tal vez. Eso encajaría fácilmente con la forma en que funcionan las cosas actualmente y aún respaldaría el caso de "campos estrictos".

¿Es el serializador el lugar adecuado para resolver este problema? Este requisito está estrechamente relacionado con las palabras clave de REST, por lo que tal vez ese sea el lugar adecuado para aplicarlo. Para admitir este enfoque, el serializador solo necesita exponer una lista de campos que acepta como entradas,

Más aparte ... ¿hay una buena discusión sobre la separación (asignación) de preocupaciones de Django en alguna parte? Tengo problemas para limitarme a respuestas compatibles con Django porque no sé la respuesta a preguntas como "¿por qué la validación es parte de la serialización?". Los documentos de serialización para 1.9 ni siquiera mencionan la validación. Y, estrictamente desde el primer principio, parece como:

  1. El modelo debe ser responsable de validar la consistencia interna y
  2. La "vista" (en este caso, el procesador en modo REST) ​​debe ser responsable de hacer cumplir las reglas comerciales (como el RFC) relacionadas con esa vista.

Si desaparece la responsabilidad de la validación, los serializadores pueden ser 100 % parciales (de forma predeterminada) y especializados para reglas de E/S como "solo lectura". Un ModelSerializer creado de esta manera admitiría una amplia variedad de vistas.

¿Es el serializador el lugar adecuado para resolver este problema?

Si.

Los documentos de serialización para 1.9 ni siquiera mencionan la validación.

La serialización incorporada de Django no es útil para Web APIS, en realidad se limita a descargar y cargar accesorios.

Conoces los supuestos arquitectónicos de Django y DRF mejor que yo, así que debo deferirte el cómo. Ciertamente, un init kwarg tiene la sensación correcta... reconfigurar el serializador "bajo demanda". La única limitación es que no se pueden reconfigurar "sobre la marcha", pero supongo que las instancias son de un solo uso, por lo que no es un problema importante.

Voy a eliminar el hito de esto por ahora. Podemos volver a evaluar después de v3.7

Depende de ustedes, pero quiero asegurarme de que tengan claro que este no es un Ticket para agregar soporte de concurrencia. El verdadero problema es que un solo serializador no puede validar correctamente tanto PUT como POST en la arquitectura actual. La concurrencia solo proporcionó la "prueba fallida".

TL; DR Puede ver por qué este problema está bloqueado al comenzar con la solución propuesta por Tom .

En resumen, la solución propuesta es hacer que todos los campos sean obligatorios para una solicitud de PUT . Hay (al menos) dos problemas con este enfoque:

  1. Los serializadores piensan en acciones, no en métodos HTTP, por lo que no hay un mapeo uno a uno. El ejemplo obvio es create porque lo comparten PUT y POST . Tenga en cuenta que create-by- PUT está deshabilitado de forma predeterminada, por lo que la solución propuesta probablemente sea mejor que nada.
  2. No necesitamos requerir todos los campos en un PUT (un sentimiento compartido por #3648, #4703). Si un campo anulable está ausente, sabemos que puede ser Ninguno. Si un campo con un valor predeterminado está ausente, sabemos que podemos usar el valor predeterminado. PUT s en realidad tienen los mismos requisitos de campo (derivados del modelo) que POST .

El problema real es cómo manejamos los datos faltantes y la propuesta básica en #3648, #4703, y aquí queda la solución correcta. Podemos admitir todos los modos HTTP (incluido crear por PUT ) si introducimos un concepto como if_missing_use_default . Mi propuesta original lo presentaba como un reemplazo de partial , pero es más fácil (y puede ser necesario) pensar en él como un concepto ortogonal.

si introducimos un concepto como if_missing_use_default.

No hay nada que impida que alguien implemente esto, o un estricto "requerir todos los campos" como una clase de serializador base, y envolverlo como una biblioteca de terceros.

Mi opinión es que un modo estricto de "requerir todos los campos" también podría convertirse en el núcleo, es un comportamiento muy claro y obvio, y puedo ver por qué sería útil.

No estoy convencido de que "permita que los campos sean opcionales, pero reemplace todo, usando los valores predeterminados del modelo, si existen". actualizándose). Si queremos un comportamiento más estricto, deberíamos tener un comportamiento más estricto.

De cualquier manera, la forma correcta de abordar esto es validarlo como un paquete de terceros y luego actualizar nuestros documentos para que podamos vincularlo.

Alternativamente, si está convencido de que nos falta un comportamiento del núcleo que nuestros usuarios realmente necesitan, puede realizar una solicitud de extracción, actualizar el comportamiento y la documentación, para que podamos evaluar los méritos de una manera muy forma concreta.

Feliz de tomar las solicitudes de incorporación de cambios como punto de partida para esto, e incluso más feliz de incluir un paquete de terceros que demuestre este comportamiento.

llegando al valor de tener una opción de comportamiento PUT-is-strict.

Esto sigue en pie. Creo que podríamos considerar ese aspecto en el núcleo, si a alguien le importa lo suficiente como para hacer una solicitud de extracción en ese sentido. Tendría que ser un comportamiento opcional.

Eso parece que presentaría un comportamiento muy contrario a la intuición (por ejemplo, campos "creados_en", que automáticamente terminan actualizándose).

Un campo created_at debe ser read_only (o excluido del serializador). En ambos casos, no cambiaría (el comportamiento normal del serializador). En el caso contrario a la intuición de que el campo no es de solo lectura en el serializador, obtendría el comportamiento contrario a la intuición de cambiarlo automáticamente.

Feliz de tomar las solicitudes de incorporación de cambios como punto de partida para esto, e incluso más feliz de incluir un paquete de terceros que demuestre este comportamiento.

Absolutamente. La variación "usar valores predeterminados" es un caso ideal para un paquete de terceros porque el cambio es un envoltorio trivial alrededor (un método de) el comportamiento existente y (si acepta el argumento predeterminado) funciona para todos los serializadores no parciales.

tomchristie cerró esto hace 4 horas

Tal vez consideraría agregar una etiqueta como "Bienvenida de relaciones públicas" o "Complemento de terceros" y dejar abiertos los problemas válidos/reconocidos como este. A menudo busco problemas abiertos para ver si ya se informó un problema y su progreso hacia la resolución. Percibo los problemas cerrados como "no válidos" o "arreglados". Mezclar algunos problemas "válidos pero cerrados" con los miles de problemas no válidos/solucionados no invita a una búsqueda eficiente (incluso si sabía que podrían estar allí).

Tal vez consideraría agregar una etiqueta como "PR Welcome" o "Complemento de terceros".

Eso sería lo suficientemente razonable, pero nos gustaría que nuestro rastreador de problemas refleje el trabajo activo o procesable en el proyecto en sí.

Es realmente importante para nosotros tratar de mantener nuestros problemas dentro de un alcance estricto. Cambiar las prioridades puede significar que, en algún momento, elijamos reabrir problemas que hemos cerrado previamente. En este momento, creo que esto se ha salido del "equipo central quiere abordar esto en el futuro inmediato".

Si surge repetidamente y sigue sin haber una solución de terceros, quizás lo reevaluaríamos.

dejando problemas válidos/reconocidos como este abiertos.

Un poco más de contexto sobre el estilo de gestión de problemas: https://www.dabapps.com/blog/sustainable-open-source-management/

¿Fue útil esta página
0 / 5 - 0 calificaciones