API CRUD: como você especifica quais campos atualizar?


9

Digamos que você tenha algum tipo de estrutura de dados, que persiste em algum tipo de banco de dados. Para simplificar, vamos chamar essa estrutura de dados Person. Agora você está encarregado de criar uma API CRUD, que permite que outros aplicativos criem, leiam, atualizem e excluam Persons. Para simplificar, vamos supor que essa API seja acessada através de algum tipo de serviço da web.

Para as partes C, R e D do CRUD, o design é simples. Usarei notação funcional do tipo C # - a implementação pode ser SOAP, REST / JSON ou outra coisa:

class Person {
    string Name;
    DateTime? DateOfBirth;
    ...
}

Identifier CreatePerson(Person);
Person GetPerson(Identifier);
void DeletePerson(Identifier);

E a atualização? A coisa natural a fazer seria

void UpdatePerson(Identifier, Person);

mas como você especificaria quais campos Personatualizar?


Soluções que eu poderia encontrar:

  • Você sempre pode exigir que uma Pessoa completa seja aprovada, ou seja, o cliente faria algo assim para atualizar a data de nascimento:

    p = GetPerson(id);
    p.DateOfBirth = ...;
    UpdatePerson(id, p);
    

    No entanto, isso exigiria algum tipo de consistência transacional ou bloqueio entre o Get e o Update; caso contrário, você poderá substituir outras alterações feitas em paralelo por outro cliente. Isso tornaria a API muito mais complicada. Além disso, é propenso a erros, uma vez que o pseudocódigo a seguir (assumindo uma linguagem de cliente com suporte a JSON)

    UpdatePerson(id, { "DateOfBirth": "2015-01-01" });
    

    - que parece correto - não apenas mudaria DateOfBirth, mas também redefiniria todos os outros campos para nulo.

  • Você pode ignorar todos os campos que são null. No entanto, como você faria a diferença entre não alterar DateOfBirth e deliberadamente alterá-lo para nulo ?

  • Mude a assinatura para void UpdatePerson(Identifier, Person, ListOfFieldNamesToUpdate).

  • Mude a assinatura para void UpdatePerson(Identifier, ListOfFieldValuePairs).

  • Use algum recurso do protocolo de transmissão: Por exemplo, você pode ignorar todos os campos não contidos na representação JSON da Pessoa. No entanto, isso geralmente requer a análise do JSON e não é possível usar os recursos internos da sua biblioteca (por exemplo, WCF).

Nenhuma das soluções parece realmente elegante para mim. Certamente, este é um problema comum, então qual é a solução de melhores práticas usada por todos?


Por que o identificador não faz parte da pessoa? Para Personinstâncias recém-criadas que ainda não são persistentes e, no caso de o identificador ser decidido como parte do mecanismo de persistência, deixe-o como nulo. Quanto à resposta, a JPA usa um número de versão; se você ler a versão 23, quando atualizar o item, se a versão no DB for 24, a gravação falhará.
SJuan76 13/09/2015

Permitir e comunicar tanto PUTe PATCHmétodos. Ao usar PATCH, apenas as chaves de envio devem ser substituídas, com PUTo objeto inteiro substituído.
Lode

Respostas:


8

Se você não possui o rastreamento de alterações como requisito para esse objeto (por exemplo, "O usuário John alterou o nome e a data de nascimento"), o mais simples seria substituir o objeto inteiro no DB por um que você receba do consumidor. Essa abordagem envolveria um pouco mais de envio de dados, mas você está evitando a leitura antes da atualização.

Se você possui um requisito de rastreamento de atividades. Seu mundo é muito mais complicado e você precisará projetar como armazenar informações sobre ações CRUD e como interceptá-las. Esse é o mundo em que você não deseja mergulhar se não tiver esse requisito.

De acordo com a substituição de valores por transações separadas, sugiro fazer uma pesquisa sobre bloqueio otimista e pessimista . Eles atenuam esse cenário comum:

  1. O objeto é lido pelo usuário1
  2. O objeto é lido pelo usuário2
  3. Objeto gravado pelo usuário1
  4. Objeto gravado pelo usuário2 e alterações substituídas pelo usuário1

Cada usuário tem uma transação diferente, portanto, SQL padrão com isso. O mais comum é o bloqueio otimista (também mencionado por @ SJuan76 no comentário sobre versões). Sua versão é seu registro no banco de dados e durante a gravação, você primeiro analisa o banco de dados se as versões corresponderem. Se as versões não corresponderem, você sabe que alguém atualizou o objeto nesse meio tempo e precisa responder com uma mensagem de erro ao consumidor sobre essa situação. Sim, você precisa mostrar esta situação ao usuário.

Observe que você precisa ler o registro real do DB antes de gravá-lo (para comparação otimizada da versão de bloqueio), portanto, implementar a lógica delta (gravar apenas valores alterados) pode não exigir uma consulta de leitura adicional antes da gravação.

A inserção da lógica delta depende muito do contrato com o consumidor, mas observe que o mais fácil para o consumidor é construir a carga útil completa em vez do delta também.


2

Temos uma API PHP em funcionamento. Para atualizações, se um campo não for enviado no objeto JSON, ele será definido como NULL. Em seguida, passa tudo para o procedimento armazenado. O procedimento armazenado tenta atualizar todos os campos com o campo = IFNULL (entrada, campo). Portanto, se apenas 1 campo estiver no objeto JSON, apenas esse campo será atualizado. Para esvaziar explicitamente um campo definido, precisamos ter o campo = '', o banco de dados atualiza o campo com uma string vazia ou com o valor padrão dessa coluna.


3
Como você define deliberadamente um campo para nulo que ainda não é nulo?
Robert Harvey

Todos os campos são definidos NOT NULL, então o padrão dos campos CHAR obter '' e todos os campos inteiros obter 0.
Jared Bernacchi

1

Especifique a lista de campos atualizados em Query String.

PUT /resource/:id?fields=name,address,dob Body { //resource body }

Implemente mesclar dados armazenados com o modelo do corpo da solicitação:

private ResourceModel MergeResourceModel(ResourceModel original, ResourceModel updated, List<string> fields)
{
    var comparer = new FieldComparer();

    foreach (
            var item in
            typeof (ResourceModel).GetProperties()
                    .Where(p => p.CustomAttributes.All(a => a.AttributeType != typeof (JsonIgnoreAttribute))))
    {
        if (fields.Contains(item.Name, comparer))
        {
            var property = typeof (ResourceModel).GetProperty(item.Name);
            property.SetValue(original, property.GetValue(updated));
        }
    }

    return original;
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.