Django-rest-framework: As chamadas PUT não "substituem totalmente o estado do recurso de destino"

Criado em 30 jun. 2016  ·  68Comentários  ·  Fonte: encode/django-rest-framework

EDIT: Para o status atual do problema, pule para https://github.com/encode/django-rest-framework/issues/4231#issuecomment -332935943

===

Estou tendo um problema ao implementar uma biblioteca de simultaneidade otimista em um aplicativo que usa DRF para interagir com o banco de dados. Eu estou tentando:

  • Confirme se o comportamento que estou vendo é atribuível ao DRF
  • Confirme se este é o comportamento pretendido
  • Determine se há alguma maneira prática de superar esse comportamento

Recentemente, adicionei a simultaneidade otimista ao meu aplicativo Django. Para salvar a pesquisa do Wiki:

  • Todo modelo tem um campo de versão
  • Quando um editor edita um objeto, ele obtém a versão do objeto que está editando
  • Quando o editor salva o objeto, o número de versão incluído é comparado ao banco de dados
  • Se as versões corresponderem, o editor atualizou o documento mais recente e o salvamento passa
  • Se as versões não corresponderem, presumimos que uma edição "conflitante" foi enviada entre o momento em que o editor carregou e salvou, portanto, rejeitamos a edição
  • Se a versão estiver faltando, não podemos fazer testes e devemos rejeitar a edição

Eu tinha uma interface do usuário herdada falando por meio de DRF. A IU legada não lidava com números de versão. Eu esperava que isso causasse erros de simultaneidade, mas isso não aconteceu. Se eu entendi a discussão em # 3648 corretamente:

  • O DRF mescla o PUT com o registro existente. Isso faz com que um ID de versão ausente seja preenchido com o ID do banco de dados atual
  • Como isso sempre fornece uma correspondência, a omissão dessa variável sempre interromperá um sistema de simultaneidade otimista que se comunica por meio de DRF
  • ~Não há opções fáceis (como tornar o campo "obrigatório") para garantir que os dados sejam enviados sempre.~ (editar: você pode contornar o problema tornando-o obrigatório, conforme demonstrado neste comentário )

Passos para reproduzir

  1. Configurar um campo de simultaneidade otimista em um modelo
  2. Crie uma nova instância e atualize várias vezes (para garantir que você não tenha mais um número de versão padrão)
  3. Envie uma atualização (PUT) por meio de DRF, excluindo o ID da versão

    Comportamento esperado

A ID de versão ausente não deve corresponder ao banco de dados e causar um problema de simultaneidade.

Comportamento real

O ID da versão ausente é preenchido pelo DRF com o ID atual para que a verificação de simultaneidade seja aprovada.

Enhancement

Todos 68 comentários

Ok, não posso prometer que serei capaz de revisar este ticket bem detalhado imediatamente, já que a próxima versão 3.4 tem prioridade. Mas obrigado por uma questão tão detalhada e bem pensada. Isso provavelmente será analisado na escala de semanas, não de dias ou meses. Se você fizer algum progresso, tiver mais alguma opinião, por favor, atualize o ticket e nos mantenha informados.

OK. Tenho certeza que meu problema é a combinação de dois fatores:

  1. O DRF não exige o campo no PUT (mesmo sendo obrigatório no modelo) pois possui um padrão (versão=0)
  2. DRF mescla os campos PUT com o objeto atual (sem injetar o padrão)

Como resultado, o DRF usa o valor atual (banco de dados) e interrompe o controle de simultaneidade. A segunda metade da questão está relacionada à discussão em #3648 (também citada acima) e há uma discussão (pré 3.x) em #1445 que ainda parece ser relevante.

Espero que um caso concreto (e cada vez mais comum) em que o comportamento padrão seja perverso seja suficiente para reabrir a discussão sobre o comportamento "ideal" de um ModelSerializer. Obviamente, tenho apenas uma polegada de profundidade no DRF, mas minha intuição é que o seguinte comportamento é apropriado para um campo obrigatório e um PUT:

  • Ao usar um serializador não parcial, devemos receber o valor, usar o padrão ou (se nenhum padrão estiver disponível) gerar um erro de validação. A validação em todo o modelo deve ser aplicada apenas às entradas/padrões.
  • Ao usar um serializador parcial, devemos receber o valor ou fazer fallback nos valores atuais. A validação em todo o modelo deve ser aplicada a esses dados combinados.
  • Acredito que o serializador "não parcial" atual seja realmente quase parcial:

    • É não parcial para campos obrigatórios e sem padrão

    • É parcial para campos que são obrigatórios e têm um padrão (já que o padrão não é usado)

    • É parcial para campos que não são obrigatórios

Não podemos alterar o marcador (1) acima ou os padrões se tornam inúteis (exigimos a entrada mesmo sabendo o padrão). Isso significa que temos que corrigir o problema alterando o nº 2 acima. Concordo com o seu argumento em #2683 que:

Padrões de modelo são padrões de modelo. O serializador deve omitir o valor e transferir a responsabilidade para Model.object.create() para lidar com isso.

Para ser consistente com essa separação de interesses, a atualização deve criar uma nova instância (delegando todos os padrões ao modelo) e aplicar os valores enviados a essa nova instância. Isso resulta no comportamento solicitado em #3648.

Tentar descrever o caminho de migração ajuda a destacar o quão estranho é o comportamento atual. O objetivo final é

  1. Corrija o ModelSerializer,
  2. Adicione um sinalizador para este estado quase parcial e
  3. Torne esse sinalizador o padrão (para compatibilidade com versões anteriores)

Qual é o nome dessa bandeira? O serializador de modelo atual é, na verdade, um serializador parcial que (um pouco arbitrariamente) requer campos que atendem à condição required==True and default==None . Não podemos usar explicitamente o sinalizador partial sem quebrar a compatibilidade com versões anteriores, então precisamos de um novo sinalizador (esperamos que temporário). Fiquei com quasi_partial , mas minha incapacidade de expressar o requisito arbitrário required==True and default==None é o motivo pelo qual está tão claro para mim que esse comportamento deve ser preterido urgentemente.

Você pode adicionar extra_kwargs no Meta do serializador, tornando version um campo obrigatório.

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

Obrigado @anoopmalev. Isso vai me manter no ramo de produção.

Depois de "dormir nele", percebo que há uma ruga extra. Tudo o que eu disse deve se aplicar aos campos do serializador. Se um campo não estiver incluído no serializador, ele não deverá ser modificado. Desta forma, todos os serilaizers são (e devem ser) parciais para os campos não incluídos. Isso é um pouco mais complicado do que o meu "criar uma nova instância" acima.

Acredito que essa questão precisa ser reduzida a uma proposta mais restrita para avançar.
Parece amplo para ser acionável em seu estado atual.
Por enquanto, estou encerrando isso - se alguém puder reduzi-lo a uma declaração concisa e acionável do comportamento desejado, podemos reconsiderar. Até então eu acho que é simplesmente muito amplo.

Aqui está uma proposta concisa... para um serializador não parcial:

  1. Para qualquer campo não listado no serializador (implícita ou explicitamente) ou marcado como somente leitura, preserve o valor existente
  2. Para todos os outros campos, use a primeira opção disponível:

    1. Preencha com o valor enviado

    2. Preencha com um padrão, incluindo um valor implícito por blank e/ou null

    3. Criar uma exceção

Para maior clareza, a validação é executada no produto final deste processo.

Ou seja, você deseja definir required=True em qualquer campo do serializador que não tenha um modelo padrão, para atualizações?

Eu tenho isso correto?

Sim (e mais). É assim que entendo a distinção partial (todos os campos opcionais) versus non-partial (todos os campos obrigatórios). A única vez que um serializador non-partial não requer um campo é a presença de um padrão (definido de forma restrita ou ampla) _já que o serializador pode usar esse padrão se nenhum valor for fornecido._

A seção em itálico é o que a DRF não está fazendo atualmente e a mudança mais importante na minha proposta. A implementação atual apenas pula o campo.

Eu tinha uma segunda proposta misturada, mas é realmente uma questão separada de quão generoso você quer ser com a ideia de um "padrão". O comportamento atual é "estrito" em que apenas default é tratado como tal. Se você _realmente_ quiser reduzir a quantidade de dados necessários, você pode tornar os campos blank=True opcionais também... assumindo que um valor ausente é um valor em branco.

@claytondaley estou usando OOL com DRF desde 2x desta forma:

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 muito bem com todo tipo de lógica de negócios e nunca causou nenhum problema.

Este foi deixado fechado por acidente? Respondi à pergunta específica e não ouvi uma razão do que havia de errado com a proposta.

@claytondaley por que o OOL deve fazer parte do DRF? Verifique meu código - ele funciona apenas em um aplicativo grande (1400 testes). VersionField é apenas um IntegerField .

Você codificou o OOL no serializador. Este é o lugar errado para fazer isso porque você tem uma condição de corrida. Atualizações paralelas (com a mesma versão anterior) passariam todas no serializador... mas apenas uma ganharia na ação de salvar.

Estou usando django-concurrency que coloca a lógica OOL na ação salvar (onde ela pertence). Basicamente UPDATE... WHERE version = submitted_version . Isso é atômico, então não há condição de corrida. No entanto, expõe uma falha na lógica de serialização::

  • Se o padrão for definido em um campo no modelo, o DRF definirá required=False . A ideia (válida) é que o DRF pode usar esse padrão se nenhum valor for enviado.
  • Se esse campo estiver ausente, no entanto, o DRF não usará o padrão. Em vez disso, ele mescla os dados enviados com a versão atual do objeto.

Quando não exigimos o campo, o fazemos porque temos um padrão a ser usado. O DRF não cumpre esse contrato porque não usa o padrão... usa o valor existente.

A questão subjacente foi discutida antes, mas eles não tinham um caso bom e concreto. OOL é esse caso ideal. O valor existente de um campo de versão sempre passa OOL para que você possa ignorar todo o sistema OOL deixando de fora a versão. Esse (obviamente) não é o comportamento desejado de um sistema OOL.

@claytondaley

Você codificou o OOL no serializador.

Eu? Você encontrou alguma lógica OOL no meu serializador ao lado do requisito de campo?

Este é o lugar errado para fazer isso porque você tem uma condição de corrida.

Sry, eu não posso ver onde está a condição de corrida aqui.

Estou usando django-concurrency que coloca a lógica OOL na ação salvar (onde ela pertence).

Eu também estou usando django-concurrency :) Mas isso é nível de modelo, não serializador. No nível do serializador, você só precisa:

  • certifique-se de que o campo _version seja sempre obrigatório (quando deveria ser)
  • certifique-se de que seu serializador saiba como lidar com erros OOL (esta parte eu omiti)
  • certifique-se de que seu apiview saiba como lidar com erros OOL e gere HTTP 409 com possível contexto diff

na verdade, não estou usando django-concurrency devido a um problema que o autor marcou como "não vai corrigir": ele ignora o OOL quando obj.save(update_fields=['one', 'two', 'tree']) é usado, o que achei uma prática ruim, então fiz um fork do pacote.

aqui está o método save ausente do serializador que mencionei anteriormente. que deve resolver todos os seus 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

Desculpe. Eu não li seu código para descobrir o que você estava fazendo. Eu vi um serializador. Obviamente, você pode contornar o problema invadindo o serializador, mas não deveria... porque a falha na lógica DRF é independente. Estou apenas usando OOL para fazer o ponto.

E você deve tentar esse código na versão mais recente do django-concurrency (usando IGNORE_DEFAULT=False ). django-concurrency também estava ignorando os valores padrão, mas enviei um patch. Havia um caso de canto estranho que eu tive que caçar para fazê-lo funcionar em casos normais.

Eu acho que isso se chama estender a funcionalidade padrão, não realmente hackear. Eu acho que o melhor lugar para esse suporte a recursos é no pacote django-concurrency .

Eu reli toda a discussão do problema e achei sua proposta muito ampla e falharia em muitos lugares (devido ao uso mágico de valores padrão de diferentes fontes sob diferentes condições). O DRF 3.x ficou muito mais fácil e previsível que o 2.x, vamos manter assim :)

Você não pode corrigir isso na camada do modelo porque está quebrado no serializador (antes de chegar ao modelo). Deixe OOL de lado... por que não exigimos um campo se default está definido?

Um serializador não parcial "requer" todos os campos (fundamentalmente) e ainda assim deixamos este passar. É um bug? Ou temos uma razão lógica?

como você pode ver no meu exemplo de código – o campo _version é sempre necessário corretamente em todos os casos possíveis.

btw, descobri que peguei emprestado o código lvl do modelo de https://github.com/gavinwahl/django-optimistic-lock e não de django-concurrency que é muito complexo por quase nenhum motivo.

... então o bug é "serializadores não parciais definir incorretamente alguns campos como não obrigatórios". Essa é a alternativa. Porque esse é o compromisso (implícito) que um serializador não parcial faz.

Posso citá-lo :

Por padrão, os serializadores devem receber valores para todos os campos obrigatórios ou gerarão erros de validação.

Isso não diz nada sobre obrigatório (exceto quando um padrão é fornecido).

(e entendo que estou falando de dois níveis diferentes, mas o ModelSerializer não deve cancelar a exigência de campos se não for assumir a responsabilidade por essa decisão)

Acho que perdi seu ponto..

(e entendo que estou falando de dois níveis diferentes, mas o ModelSerializer não deve cancelar a exigência de campos se não for assumir a responsabilidade por essa decisão)

O que há de errado com isso?

OK, deixe-me tentar um ângulo diferente.

  • Suponha que eu tenha um serializador de modelo não parcial (editar: todos os padrões) que cobre todos os campos do meu modelo.

Se um CREATE ou UPDATE com os mesmos dados produzir um objeto diferente (menos o ID)

Você pode descrever suas ideias usando algum modelo e serializador realmente simples e algumas linhas que mostram o comportamento com falha / esperado?

Vou montar algo amanhã, pois está ficando tarde aqui... mas quanto mais fundo eu aprofundo, mais sentido o #3648 faz para um serializador não parcial. Enquanto isso, por que um ModelSerializer não exige todos os campos do modelo? Talvez seu raciocínio seja diferente do meu.

ModelSerializer inspeciona o modelo limitado e decide se deve ser necessário, não é?

Não me refiro mecanicamente como . A suposição básica para um serializador não parcial é exigir tudo (citado acima). Se get_field_kwargs vai se desviar dessa suposição (especificamente, aqui ), deve ter um bom motivo. Qual é esse motivo?

Minha resposta preferida é a que continuo dando, "porque ele pode usar esse padrão se nenhum valor for enviado" (mas o DRF precisa realmente usar o padrão). Existe outra resposta que estou perdendo? Uma razão pela qual os campos com padrões não devem ser obrigatórios?

Obviamente, prefiro a solução "completa". No entanto, admito que há uma segunda resposta. Poderíamos exigir esses campos por padrão. Isso elimina o caso especial (atualmente arbitrário). Simplifica/reduz o código. É internamente consistente. É aborda a minha preocupação.

Basicamente, torna o serializador não parcial verdadeiramente não parcial.

Agora eu pelo menos sei o que você quer dizer. você verificou qual é o comportamento do ModelForm nesse caso? (Não posso fazer isso sozinho no celular)

A documentação do Django diz que 'em branco' controla se o campo é obrigatório ou não. Sugiro que você abra um ticket separado para esse problema, pois este contém muitos comentários não relacionados. Na minha opinião, modelserializer pode funcionar como modelform: controles de opção em branco necessários, 'null' informa se None é uma entrada aceitável e 'default' não tem efeito nessa lógica.

Estou disposto a abrir um segundo ticket, mas estou preocupado que o espaço em branco exija um código semelhante. Do grupo de discussão do django :

se pegarmos um formulário de modelo existente e um modelo que funcione, adicione um campo de caractere opcional ao modelo, mas não adicione um campo correspondente ao modelo HTML (por exemplo, erro humano, esqueci de um modelo, não disse ao autor do modelo para fazer uma mudança, não percebeu que uma mudança precisava ser feita em um template), quando esse formulário é submetido, o Django irá assumir que o usuário forneceu um valor de string vazio para o campo ausente e salvá-lo no modelo, apagando qualquer valor existente .

Para sermos consistentes, teríamos a obrigação de cumprir a segunda metade do contrato, definindo o valor ausente em branco. Isso é um pouco menos problemático porque um espaço em branco pode ser preenchido sem referência a um modelo, mas é muito semelhante (e, acho, consistente com #3648).

@tomchristie você pode dar algumas informações curtas sobre isso: Por que o estado required depende da propriedade do campo do modelo defaults ?

Por que o estado obrigatório depende da propriedade de padrões do campo do modelo?

Simplesmente isto: se um campo de modelo tiver um padrão, você poderá omitir fornecê-lo como entrada.

Na verdade eu concordo com esse comportamento. ModelForm apesar do código estar fazendo o mesmo (o html gerado fornecerá os padrões). Se o DRF tiver uma lógica diferente, o 'padrão' nunca será aplicado. Eu terminei com este problema.

@pySilver na verdade, aqui está o comportamento do 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 maior clareza, o material ainda é chamado de "parcial" porque o _update_ é parcial. Eu também estava testando uma atualização completa ("full"), mas o código era desnecessário para mostrar o comportamento:

# 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 requer essa entrada mesmo que tenha um padrão. Este é um dos dois comportamentos internamente consistentes.

Por que o estado obrigatório depende da propriedade de padrões do campo do modelo?

Simplesmente isto: se um campo de modelo tiver um padrão, você poderá omitir fornecê-lo como entrada.

@tomchristie concorda em princípio. Mas qual é o comportamento esperado?

  • Ao criar, recebo o padrão (trivial, todos concordam que isso está certo)
  • Na atualização, o que devo obter?

Parece-me que eu deveria obter o padrão na atualização também. Não vejo por que um serializador não parcial deve se comportar de maneira diferente nos dois casos. Não parcial significa que estou enviando o registro "completo". Assim, o registro completo deve ser substituído.

Eu esperaria que o valor permanecesse inalterado se não fosse fornecido na atualização. Eu vejo o ponto, mas sobrescrever de forma transparente com o valor padrão seria contra-intuitivo do meu ponto de vista.

(Se alguma coisa, eu acho que provavelmente seria melhor que todas as atualizações fossem semânticas parciais para todos os campos - PUT ainda seria idempotente, que é o aspecto importante, embora possivelmente estranho alterar o comportamento atual)

Eu certamente não compartilho suas preferências; Eu quero que todas as minhas interfaces sejam rígidas, a menos que eu as faça deliberadamente de outra forma. No entanto, sua distinção PARCIAL vs. NÃO-PARCIAL já fornece (em teoria) o que nós dois queremos.

Acredito que parcial se comporte exatamente como você deseja:

  • ATUALIZAÇÕES são 100% parciais
  • CREATEs (eu suponho) são parciais em relação a default e blank (exceções lógicas). Em todos os outros casos, as restrições do modelo/banco de dados são vinculadas.

Estou apenas tentando obter consistência no serializador não parcial. Se você eliminar o caso especial para default , seus serializadores não parciais existentes se tornarão o serializador estrito que eu quero. Eles também atingem a paridade com ModelForm.

Percebo que isso cria uma pequena descontinuidade dentro do projeto, mas não é a primeira vez que alguém faz uma mudança como essa. Adicione um sinalizador "herdado" padrão para o comportamento atual, adicione um aviso (que o comportamento padrão será alterado) e altere o padrão em uma versão principal subsequente.

Mais importante, se você quiser que seus serializadores sejam o novo de fato para o Django, você acabará fazendo essa mudança de qualquer maneira. O número de pessoas convertendo do ModelForm excederá amplamente a base de usuários existente e eles esperarão pelo menos essa mudança.

Inserindo meus dois centavos:
Estou inclinado a concordar com @claytondaley. PUT é uma substituição de recurso idempotente, PATCH é uma atualização do recurso existente. Tome o seguinte exemplo:

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

Novos perfis sensatamente têm a função de membro padrão. Vamos atender os seguintes pedidos:

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

Como está atualmente, após o primeiro PUT, os dados do perfil conteriam {'username': 'curly', 'role': 'member'} . Após o segundo PUT, você teria {'username': 'curly', 'role': 'admin'} . Isso não quebra a idempotência? (Não tenho certeza - estou perguntando legitimamente)

Editar:
Acho que todos estão na mesma página sobre a semântica do PATCH.

Após o segundo PUT, você teria {'username': 'curly', 'role': 'admin'}

Eu, pessoalmente, ficaria surpreso se a função voltasse ao padrão (embora eu veja o motivo dessa discussão de objeto replace , eu nunca tive nenhum problema do mundo real com isso ainda)

eu nunca tive nenhum problema do mundo real com ele ainda

O mesmo aqui, mas até agora nossos projetos contaram com PATCH :)
Dito isso, o caso de uso do OP com versão de modelo para lidar com simultaneidade faz sentido para mim. Eu esperaria que PUT usasse o valor padrão (se o valor for omitido), levantando a exceção de simultaneidade.

Deixe-me começar reconhecendo que um serializador não precisa necessariamente seguir o RFC RESTful. No entanto, eles devem pelo menos oferecer modos que _são_ compatíveis -- especialmente em um pacote que oferece suporte REST.

Meu argumento original era de primeiros princípios, mas a RFC (seção 4.3.4) diz especificamente (grifo nosso):

A diferença fundamental entre os métodos POST e PUT é destacada pela intenção diferente para a representação anexada. O recurso de destino em uma solicitação POST destina-se a manipular a representação incluída de acordo com a semântica do próprio recurso, enquanto a representação incluída em uma solicitação PUT é definida como a substituição do estado do recurso de destino.
...
Um servidor de origem que permite PUT em um determinado recurso de destino DEVE enviar uma resposta 400 (Bad Request) a uma solicitação PUT que contenha um campo de cabeçalho Content-Range (Seção 4.2 de [RFC7233]), pois a carga útil provavelmente será conteúdo parcial que foi colocado erroneamente como uma representação completa . Atualizações parciais de conteúdo são possíveis direcionando um recurso identificado separadamente com estado que se sobrepõe a uma parte do recurso maior ou usando um método diferente que foi definido especificamente para atualizações parciais (por exemplo, o método PATCH definido em [RFC5789])

Portanto, um PUT nunca deve ser parcial (veja também aqui ). No entanto, a seção sobre PUT também esclarece:

O método PUT solicita que o estado do recurso de destino seja criado ou substituído pelo estado definido pela representação incluída na carga útil da mensagem de solicitação. Um PUT bem-sucedido de uma determinada representação sugeriria que um GET subsequente nesse mesmo recurso de destino resultaria no envio de uma representação equivalente em uma resposta 200 (OK).

O ponto sobre o GET (embora não seja obrigatório) defende minha solução de "compromisso". Embora a injeção de espaços em branco/padrões seja conveniente, não forneceria esse comportamento. O prego no caixão provavelmente é que essa solução minimiza a confusão, pois não haverá campos ausentes para levantar dúvidas.

Obviamente, PATCH é uma opção especificada para atualizações parciais, mas é descrito como um "conjunto de instruções" em vez de apenas um PUT parcial, então sempre me deixa um pouco ansioso. A seção sobre POST (4.3.3) realmente afirma:

O método POST solicita que o recurso de destino processe a representação incluída na solicitação de acordo com a semântica específica do próprio recurso. Por exemplo, POST é usado para as seguintes funções (entre outras):

  • Fornecer um bloco de dados, como os campos inseridos em um formulário HTML, para um processo de manipulação de dados;

...

  • Anexando dados à(s) representação(ões) existente(s) de um recurso.

Eu acho que há um argumento para usar POST para atualizações parciais desde:

  • conceitualmente, alterar dados não é diferente de anexar
  • POST tem permissão para usar suas próprias regras para que essas regras possam ser uma atualização parcial
  • esta operação pode ser facilmente distinguida de um CREATE pela presença de um ID

Mesmo que o DRF não aspire a conformidade total, precisamos de um serializador que seja compatível com a operação PUT spec (ou seja, substituindo o objeto inteiro). A resposta mais simples (e claramente menos confusa) é exigir todos os campos. Também sugere que PUT deve ser não parcial por padrão e que atualizações parciais devem usar uma palavra-chave diferente (PATCH ou mesmo POST).

Acho que acabei de encontrar meu primeiro problema de PUT ao migrar nosso aplicativo para 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)
        ]

Isso faz com que meu .validated_data contenha dados que não forneci na solicitação PUT e não forneci manualmente no serializador. Os valores foram recuperados de default= no nível do serializador. Então, basicamente, embora pretendo atualizar um campo específico, também sobrescrevo alguns desses campos com valores padrão do nada.

Feliz por mim, estou usando o ModelSerializer personalizado, para que eu possa corrigir o problema facilmente.

@pySilver Não entendo o conteúdo do último comentário.

@rpkilby "Vamos atender as seguintes solicitações... Isso não quebra a idempotência"

Não, cada solicitação PUT é idempotente, pois pode ser repetida várias vezes, resultando no mesmo estado. Isso não significa que, se alguma outra parte do estado foi modificada nesse meio tempo, ela será redefinida de alguma forma.

Aqui estão algumas opções diferentes para o comportamento PUT .

  • Os campos são obrigatórios, a menos que required=False ou tenham um default . (Existir)
  • Todos os campos são necessários. (Semântica mais rigorosa e alinhada de atualização completa _mas_ estranha porque é realmente mais rigorosa do que a semântica de criação inicial para POST)
  • Nenhum campo é obrigatório (ou seja, apenas espelhar o comportamento do PATCH)

Claro que não há uma resposta _absoluta_ correta, mas acredito que temos o comportamento mais prático como está atualmente.

Acredito que alguns casos de uso podem achar problemático se houver um campo que não precise ser fornecido para solicitações POST , mas, posteriormente, para solicitações PUT . Além disso, PUT-as-create é em si uma operação válida, então, novamente, seria estranho se isso tivesse uma semântica de "exigência" diferente para POST.

Se alguém quiser levar isso adiante, sugiro _fortemente_ começar como um pacote de terceiros, que implementa diferentes classes de serializador de base. Podemos, então, vincular a isso a partir da documentação dos serializadores. Se o caso for bem feito, podemos considerar a adaptação do comportamento padrão em algum momento no futuro.

@tomchristie o que eu quero dizer:

Eu tenho um serializador com campo readonly language e um 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 :(
  • Não passei language na solicitação e não espero ver isso nos dados validados. Sendo passado para update , ele substituiria minha instância por padrões, apesar do fato de que o objeto já possui algum valor não padrão atribuído.
  • Seria menos problemático se validated_data['language'] fosse book.language nesse caso.

@pySilver - Sim, isso foi resolvido em https://github.com/tomchristie/django-rest-framework/pull/4346 apenas hoje.

Acontece que você não precisa de default= no campo do serializador no exemplo que você tem, pois você tem um padrão no ModelField .

@tomchristie Você pelo menos concorda que o comportamento PUT atual não é a especificação RFC? E que ambas as minhas sugestões (exigir todos ou injetar padrões) fariam isso?

@tomchristie ótimas notícias!

Acontece que você não precisa de default= no campo do serializador no exemplo que você tem, pois você tem um padrão no ModelField.

Sim, eu só queria torná-lo super explícito para demonstração.

Finalmente, está se dando conta que @tomchristie não está argumentando a favor/contra o comportamento do serializador isoladamente. Acredito que suas objeções decorrem (implicitamente) do requisito de que um único serializador suporte todos os modos REST. Isso se mostra em suas reclamações sobre como um serializador estrito afetará um POST. Como os modos REST são incompatíveis, a solução atual é um serializador que não é especificado para nenhum modo único.

Se essa é a verdadeira raiz da objeção, vamos enfrentá-la de frente. Como um único serializador pode fornecer comportamento de especificação para todos os modos REST? Minha resposta improvisada é que PARCIAL vs. NÃO PARCIAL é implementado no nível errado:

  • Temos serializadores parciais e não parciais. Essa abordagem significa que precisamos de vários serializadores para dar suporte ao comportamento de especificação para todos os modos.
  • Na verdade, precisamos de validação parcial versus não parcial (ou algo nesse sentido). Os diferentes modos REST precisam solicitar diferentes modos de validação do serializador.

Para fornecer separação de preocupações, um serializador não deve conhecer o modo REST para que não possa ser implementado como um serializador de terceiros (nem, suspeito, o serializador sequer tem acesso ao modo). Em vez disso, o DRF deve passar uma informação extra para o serializador (aproximadamente replace=True para PUT ). O serializador pode decidir como implementar isso (exigir todos os campos ou injetar os padrões).

Obviamente, esta é apenas uma proposta grosseira, mas talvez ela resolva o impasse.

Além disso, PUT-as-create é em si uma operação válida, então, novamente, seria estranho se isso tivesse uma semântica de "exigência" diferente para POST.

Concordo que você pode criar com um PUT, mas discordo que a semântica seja a mesma. PUT funciona em um recurso específico:

O método PUT solicita que o estado do recurso de destino seja criado ou substituído pelo estado definido pela representação incluída na carga útil da mensagem de solicitação.

Acredito, portanto, que a semântica de criação realmente difere:

  • PUBLICARpara /citizen/ espera que um SSN (número de segurança social) seja gerado
  • COLOCARpara /citizen/<SSN> atualiza os dados para um SSN específico. Se não houver dados nesse SSN, isso resultará em uma criação.

Como o "id" deve ser incluído no URI de PUT, você pode tratá-lo conforme necessário. Por outro lado, o "id" é opcional em um POST.

Como o "id" deve ser incluído no URI de PUT, você pode tratá-lo conforme necessário. Por outro lado, o "id" é opcional em um POST.

De fato. Eu estava me referindo especificamente ao fato de que a mudança proposta de "fazer PUT exigir estritamente _todos_ campos" significaria que PUT-as-create teria um comportamento diferente para POST-as-create wrt. se os campos são obrigatórios ou não.

Dito isto, estou chegando ao valor de ter uma opção de comportamento PUT-is-strict.

(Reforce que os campos _all_ sejam estritamente obrigatórios neste caso, imponha que os campos _no_ sejam obrigatórios no PATCH e use o sinalizador required= para POST)

Como um único serializador pode fornecer comportamento de especificação para todos os modos REST?

Podemos diferenciar entre criar, atualizar e atualizar parcial dado como o serializador é instanciado, então não acho que isso seja um problema.

Você já disse que pode create usando PUT ou POST . Eles têm semânticas e requisitos diferentes, então create precisa ser independente do modo REST. Acho que a distinção realmente acontece como parte de is_valid . Pedimos um modo de validação específico:

  • sem validação de presença de campo (PATCH)
  • validação baseada em sinalizadores required (POST)
  • validação de presença de campo estrita (PUT)

Ao manter a lógica específica da palavra-chave fora das operações CRUD, também reduzimos o acoplamento entre o serializador e o DRF. Se os modos de validação fossem configuráveis, seriam totalmente de uso geral (mesmo que implementássemos apenas 3 casos específicos para nossas 3 palavras-chave).

Você está fazendo um bom trabalho ao me argumentar sobre essa funcionalidade. :)

A diferença de "modos de validação" ao chamar .is_valid() é uma reviravolta que não vai acontecer.

Nós _poderíamos_ considerar uma contraparte 'complete=True' para a unidade kwarg 'partial=True' existente, talvez. Isso se encaixaria facilmente com a forma como as coisas funcionam atualmente e ainda suportaria o caso de "campos restritos".

O serializador é o lugar certo para resolver esse problema? Esse requisito está fortemente acoplado às palavras-chave REST, então talvez seja o lugar certo para aplicá-lo. Para dar suporte a essa abordagem, o serializador precisa apenas expor uma lista de campos que ele aceita como entradas,

Mais um aparte... existe uma boa discussão sobre a separação (alocação) de preocupações do Django em algum lugar? Estou tendo problemas para me limitar a respostas amigáveis ​​ao Django porque não sei a resposta para perguntas como "por que a validação faz parte da serialização". Os documentos de serialização para 1.9 nem mencionam validação. E, estritamente do primeiro princípio, parece que:

  1. O modelo deve ser responsável por validar a consistência interna e
  2. A "visualização" (neste caso, o processador do modo REST) ​​deve ser responsável por impor regras de negócios (como o RFC) relacionadas a essa visualização.

Se a responsabilidade pela validação desaparecer, os serializadores podem ser 100% parciais (por padrão) e especializados para regras de E/S como "somente leitura". Um ModelSerializer construído dessa maneira ofereceria suporte a uma ampla variedade de exibições.

O serializador é o lugar certo para resolver esse problema?

sim.

Os documentos de serialização para 1.9 nem mencionam validação.

A serialização embutida do Django não é útil para Web APIS, é realmente limitada a despejar e carregar fixtures.

Você conhece as suposições arquitetônicas do Django e do DRF melhor do que eu, então devo adiar o como. Certamente um kwarg init tem a sensação certa... reconfigurando o serializador "sob demanda". A única limitação é que eles não podem ser reconfigurados "on the fly", mas presumo que as instâncias sejam de uso único, portanto, isso não é um problema significativo.

Vou de-marco isso por enquanto. Podemos reavaliar após a v3.7

Até vocês, mas eu quero ter certeza de que você está claro que este não é um Ticket para adicionar suporte de simultaneidade. O problema real é que um único serializador não pode validar corretamente um PUT e POST na arquitetura atual. A simultaneidade apenas forneceu o "teste de falha".

TL;DR Você pode ver por que esse problema está bloqueado começando na correção proposta por Tom .

Em resumo, a solução proposta é tornar todos os campos obrigatórios para uma requisição PUT . Existem (pelo menos) dois problemas com essa abordagem:

  1. Os serializadores pensam em ações, não em métodos HTTP, portanto, não há um mapeamento um para um. O exemplo óbvio é create porque é compartilhado por PUT e POST . Observe que create-by- PUT está desabilitado por padrão, então a correção proposta é provavelmente melhor do que nada.
  2. Não precisamos exigir todos os campos em PUT (um sentimento compartilhado por #3648, #4703). Se um campo nillable estiver ausente, sabemos que pode ser None. Se um campo com um padrão estiver ausente, sabemos que podemos usar o padrão. PUT s na verdade têm os mesmos requisitos de campo (derivados do modelo) que POST .

A verdadeira questão é como lidamos com dados ausentes e a proposta básica em #3648, #4703, e aqui permanece a solução certa. Podemos suportar todos os modos HTTP (incluindo create-by- PUT ) se introduzirmos um conceito como if_missing_use_default . Minha proposta original o apresentava como um substituto para partial , mas é mais fácil (e pode ser necessário) pensar nele como um conceito ortogonal.

se introduzirmos um conceito como if_missing_use_default.

Não há nada que impeça alguém de implementar isso ou um estrito "exigir todos os campos" como uma classe de serializador de base e empacotar isso como uma biblioteca de terceiros.

Minha opinião é que um modo estrito "exigir todos os campos" também pode ser capaz de torná-lo no núcleo, é um comportamento óbvio muito claro, e posso ver por que isso seria útil.

Não estou convencido de que "permitir que os campos sejam opcionais, mas substituam tudo, usando padrões de modelo se existirem" - Parece que apresentaria um comportamento muito contra-intuitivo (por exemplo, campos "created_at", que terminam automaticamente atualizando-se). Se queremos um comportamento mais rigoroso, devemos apenas ter um comportamento mais rigoroso.

De qualquer forma, a maneira correta de abordar isso é validá-lo como um pacote de terceiros e atualizar nossos documentos para que possamos vincular a ele.

Alternativamente, se você está convencido de que está faltando um comportamento do núcleo que nossos usuários realmente precisam, então você pode fazer um pull request, atualizando o comportamento e a documentação, para que possamos avaliar os méritos de uma forma muito maneira concreta.

Feliz em receber solicitações de pull como ponto de partida para isso e ainda mais feliz em incluir um pacote de terceiros demonstrando esse comportamento.

chegando ao valor de ter uma opção de comportamento PUT-is-strict.

Isso ainda está de pé. Acho que poderíamos considerar esse aspecto no núcleo, se alguém se importasse o suficiente para fazer um pull request nesse sentido. Precisaria ser um comportamento opcional.

Isso parece apresentar um comportamento muito contra-intuitivo (por exemplo, campos "created_at", que automaticamente acabam se atualizando).

Um campo created_at deve ser read_only (ou excluído do serializador). Em ambos os casos, seria inalterado (o comportamento normal do serializador). No caso contra-intuitivo em que o campo não é somente leitura no serializador, você obteria o comportamento contra-intuitivo de alterá-lo automaticamente.

Feliz em receber solicitações de pull como ponto de partida para isso e ainda mais feliz em incluir um pacote de terceiros demonstrando esse comportamento.

Absolutamente. A variação "use defaults" é um caso ideal para um pacote de terceiros porque a alteração é um wrapper trivial em torno (um método) do comportamento existente e (se você comprar o argumento defaults) funciona para todos os serializadores não parciais.

tomchristie fechou há 4 horas

Talvez você considere adicionar um rótulo como "PR Welcome" ou "3rd Party Plugin" e deixar problemas válidos/reconhecidos como este em aberto. Costumo pesquisar questões em aberto para ver se um problema já foi relatado e seu progresso em direção à resolução. Percebo questões encerradas como "inválidas" ou "corrigidas". Misturar alguns problemas "válidos, mas fechados" em milhares de problemas inválidos/corrigidos não convida a uma pesquisa eficiente (mesmo que você saiba que eles podem estar lá).

Talvez você considere adicionar um rótulo como "PR Welcome" ou "3rd Party Plugin"

Isso seria bastante razoável, mas gostaríamos que nosso rastreador de problemas refletisse o trabalho ativo ou acionável no próprio projeto.

É muito importante para nós tentarmos manter nossos problemas bem delimitados. Alterar as prioridades pode significar que, em algum momento, optamos por reabrir problemas que fechamos anteriormente. Agora eu acho que isso caiu fora do "o time principal quer resolver isso no futuro imediato".

Se surgir repetidamente e continuar sem solução de terceiros, talvez a reavaliemos.

deixando questões válidas/reconhecidas como esta em aberto.

Um pouco mais de contexto sobre o estilo de gerenciamento de problemas - https://www.dabapps.com/blog/sustainable-open-source-management/

Esta página foi útil?
0 / 5 - 0 avaliações