Prática recomendada para atualizações parciais em um serviço RESTful


208

Estou escrevendo um serviço RESTful para um sistema de gerenciamento de clientes e estou tentando encontrar a melhor prática para atualizar parcialmente os registros. Por exemplo, desejo que o chamador possa ler o registro completo com uma solicitação GET. Mas, para atualizá-lo, apenas certas operações no registro são permitidas, como alterar o status de ENABLED para DISABLED. (Eu tenho cenários mais complexos que isso)

Não quero que o chamador envie o registro inteiro apenas com o campo atualizado por razões de segurança (também parece um exagero).

Existe uma maneira recomendada de construir os URIs? Ao ler os livros REST, as chamadas no estilo RPC parecem desaprovadas.

Se a chamada a seguir retornar o registro completo do cliente com o ID 123

GET /customer/123
<customer>
    {lots of attributes}
    <status>ENABLED</status>
    {even more attributes}
</customer>

como devo atualizar o status?

POST /customer/123/status
<status>DISABLED</status>

POST /customer/123/changeStatus
DISABLED

...

Atualização : Para aumentar a pergunta. Como alguém incorpora 'chamadas de lógica de negócios' em uma API REST? Existe uma maneira acordada de fazer isso? Nem todos os métodos são CRUD por natureza. Alguns são mais complexos, como ' sendEmailToCustomer (123) ', ' mergeCustomers (123, 456) ', ' countCustomers () '

POST /customer/123?cmd=sendEmail

POST /cmd/sendEmail?customerId=123

GET /customer/count 

3
Para responder sua pergunta sobre "chamadas da lógica de negócios", aqui está um post sobre o POSTpróprio Roy Fielding: roy.gbiv.com/untangled/2009/it-is-okay-to-use-post onde a idéia básica é: se não houver é um método (como GETou PUT) ideal para o seu uso operacional POST.
rojoca 23/03

Isso é basicamente o que acabei fazendo. Faça chamadas REST para recuperar e atualizar recursos conhecidos usando GET, PUT, DELETE. POST para adicionar novos recursos e POST com alguma URL descritiva para chamadas de lógica de negócios.
magiconair

Qualquer que seja sua decisão, se essa operação não fizer parte da resposta GET, você não terá um serviço RESTful. Eu não estou vendo isso aqui
MStodd 17/07/2015

Respostas:


69

Você basicamente tem duas opções:

  1. Use PATCH(mas observe que você precisa definir seu próprio tipo de mídia que especifica exatamente o que acontecerá)

  2. Use POSTpara um sub-recurso e retorne 303 Consulte Outro com o cabeçalho Localização apontando para o recurso principal. A intenção do 303 é informar ao cliente: "Eu executei seu POST e o efeito foi que algum outro recurso foi atualizado. Consulte o cabeçalho do local para qual recurso foi". O POST / 303 destina-se a adições iterativas aos recursos para construir o estado de algum recurso principal e é um ajuste perfeito para atualizações parciais.


OK, o POST / 303 faz sentido para mim. PATCH e MERGE Não consegui encontrar na lista de verbos HTTP válidos, o que exigiria mais testes. Como construir um URI se eu quiser que o sistema envie um email para o cliente 123? Algo como uma chamada de método RPC pura que não altera o estado do objeto. Qual é a maneira RESTful de fazer isso?
magiconair

Não entendi a pergunta sobre o URI do email. Deseja implementar um gateway ao qual você possa POST para que ele envie um email ou está procurando mailto: customer.123@service.org?
Jan Algermissen

15
Nem o REST nem o HTTP têm nada a ver com o CRUD, exceto por algumas pessoas que equiparam os métodos HTTP ao CRUD. O REST trata da manipulação do estado do recurso, transferindo representações. Seja o que for que você deseja alcançar, transfira uma representação para um recurso com a semântica apropriada. Cuidado com os termos 'chamadas de método puro' ou 'lógica de negócios', pois implicam com muita facilidade 'HTTP é para transporte'. Se você precisar enviar um email, POST para um recurso de gateway, se precisar mesclar para contas, crie um novo e representações POST dos outros dois etc.
Jan Algermissen

9
Veja também como o Google faz: googlecode.blogspot.com/2010/03/…
Marius

4
PATRIMÔNIO williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot [{"op": "test", "path": "/ a / b / c", "value" : "foo"}, {"op": "remove", "path": "/ a / b / c"}, {"op": "add", "path": "/ a / b / c" , "value": ["foo", "bar"]}, {"op": "replace", "path": "/ a / b / c", "value": 42}, {"op": "mover", "de": "/ a / b / c", "caminho": "/ a / b / d"}, {"op": "copiar", "de": "/ a / b / d "," caminho ":" / a / b / e "}]
intotecho

48

Você deve usar o POST para atualizações parciais.

Para atualizar os campos do cliente 123, faça um POST para / customer / 123.

Se você deseja atualizar apenas o status, também pode COLOCAR em / customer / 123 / status.

Geralmente, as solicitações GET não devem ter efeitos colaterais, e PUT é para gravar / substituir todo o recurso.

Isso segue diretamente do HTTP, como pode ser visto aqui: http://en.wikipedia.org/wiki/HTTP_PUT#Request_methods


1
Não @ John Saunders POST não tem que criar necessariamente um novo recurso que é acessível a partir de um URI: tools.ietf.org/html/rfc2616#section-9.5
wsorenson

10
@wsorensen: Eu sei que ele não precisa resultar em um novo URL, mas ainda pensei que um POST /customer/123deveria criar a coisa óbvia que está logicamente sob o cliente 123. Talvez um pedido? PUT /customer/123/statusparece fazer mais sentido, assumindo que o POST /customerscriou implicitamente um status(e supondo que seja REST legítimo).
John Saunders

1
@ John Saunders: na prática, se queremos atualizar um campo em um recurso localizado em um determinado URI, o POST faz mais sentido do que PUT e, na falta de um UPDATE, acredito que ele seja frequentemente usado nos serviços REST. O POST para / customers pode criar um novo cliente, e um PUT para / customer / 123 / status pode se alinhar melhor com a palavra da especificação, mas, quanto às melhores práticas, acho que não há motivo para não postar para / customer / 123 para atualizar um campo - é conciso, faz sentido e não é estritamente contrário a nada na especificação.
wsorenson

8
Solicitações POST não devem ser idempotentes? Certamente atualizar uma entrada é idempotente e, portanto, deve ser um PUT?
Martin Andersson

1
As solicitações @MartinAndersson POSTnão precisam ser não-idempotentes. E, como mencionado, PUTdeve substituir um recurso inteiro.
Halle Knast

10

Você deve usar PATCH para atualizações parciais - usando documentos json-patch (consulte http://tools.ietf.org/html/draft-ietf-appsawg-json-patch-08 ou http://www.mnot.net/ blog / 2012/09/05 / patch ) ou a estrutura de patches XML (consulte http://tools.ietf.org/html/rfc5261 ). Na minha opinião, porém, o json-patch é o mais adequado para o seu tipo de dados corporativos.

PATCH com documentos de correção JSON / XML possui semântica direta muito direta para atualizações parciais. Se você começar a usar o POST, com cópias modificadas do documento original, para atualizações parciais, logo encontrará problemas nos quais você deseja que valores ausentes (ou melhor, valores nulos) representem "ignorar esta propriedade" ou "definir essa propriedade como valor vazio "- e isso leva a um buraco de soluções invadidas que, no final, resultam no seu próprio tipo de formato de patch.

Você pode encontrar uma resposta mais detalhada aqui: http://soabits.blogspot.dk/2013/01/http-put-patch-or-post-partial-updates.html .


Observe que, enquanto isso, os RFCs para json-patch e xml-patch foram finalizados.
Botchniaque

8

Estou com um problema semelhante. PUT em um sub-recurso parece funcionar quando você deseja atualizar apenas um único campo. No entanto, às vezes você deseja atualizar várias coisas: Pense em um formulário da Web representando o recurso com a opção de alterar algumas entradas. O envio do formulário pelo usuário não deve resultar em várias PUTs.

Aqui estão duas soluções em que consigo pensar:

  1. PUT com todo o recurso. No lado do servidor, defina a semântica que uma PUT com todo o recurso ignora todos os valores que não foram alterados.

  2. faça um PUT com um recurso parcial. No lado do servidor, defina a semântica disso como uma mesclagem.

2 é apenas uma otimização de largura de banda de 1. Às vezes 1 é a única opção se o recurso define que alguns campos são campos obrigatórios (pense em proto buffers).

O problema com essas duas abordagens é como limpar um campo. Você precisará definir um valor nulo especial (especialmente para proto-buffers, pois os valores nulos não são definidos para proto-buffers) que causarão a limpeza do campo.

Comentários?


2
Isso seria mais útil se publicado como uma pergunta separada.
Decot

6

Para modificar o status, acho que uma abordagem RESTful é usar um sub-recurso lógico que descreva o status dos recursos. Esse IMO é bastante útil e limpo quando você tem um conjunto reduzido de status. Isso torna sua API mais expressiva sem forçar as operações existentes para o recurso do cliente.

Exemplo:

POST /customer/active  <-- Providing entity in the body a new customer
{
  ...  // attributes here except status
}

O serviço POST deve retornar o cliente recém-criado com o ID:

{
    id:123,
    ...  // the other fields here
}

O GET para o recurso criado usaria o local do recurso:

GET /customer/123/active

Um GET / customer / 123 / inativo deve retornar 404

Para a operação PUT, sem fornecer uma entidade Json, apenas atualizará o status

PUT /customer/123/inactive  <-- Deactivating an existing customer

Fornecer uma entidade permitirá que você atualize o conteúdo do cliente e atualize o status ao mesmo tempo.

PUT /customer/123/inactive
{
    ...  // entity fields here except id and status
}

Você está criando um sub-recurso conceitual para o recurso do cliente. Também é consistente com a definição de Roy Fielding de um recurso: "... Um recurso é um mapeamento conceitual para um conjunto de entidades, não a entidade que corresponde ao mapeamento em qualquer ponto específico do tempo ..." Nesse caso, o o mapeamento conceitual é cliente ativo para cliente com status = ATIVO.

Leia operação:

GET /customer/123/active 
GET /customer/123/inactive

Se você fizer essas chamadas uma após a outra, retornar o status 404, a saída bem-sucedida pode não incluir o status implícito. É claro que você ainda pode usar GET / customer / 123? Status = ACTIVE | INACTIVE para consultar diretamente o recurso do cliente.

A operação DELETE é interessante, pois a semântica pode ser confusa. Mas você tem a opção de não publicar essa operação para esse recurso conceitual ou usá-la de acordo com sua lógica de negócios.

DELETE /customer/123/active

Esse pode levar seu cliente a um status DELETED / DISABLED ou ao status oposto (ATIVO / INATIVO).


Como você chega ao sub-recurso?
MStodd

I reformulado o respondeu tentando torná-la mais clara
raspacorp

5

Itens a serem adicionados à sua pergunta aumentada. Acho que muitas vezes você pode projetar perfeitamente ações de negócios mais complicadas. Mas você precisa revelar o estilo de método / procedimento e pensar mais em recursos e verbos.

envios de correio


POST /customers/123/mails

payload:
{from: x@x.com, subject: "foo", to: y@y.com}

A implementação desse recurso + POST enviaria o email. se necessário, você pode oferecer algo como / customer / 123 / outbox e oferecer links de recursos para / customer / mails / {mailId}.

contagem de clientes

Você poderia lidar com isso como um recurso de pesquisa (incluindo metadados de pesquisa com informações de paginação e número encontrado, o que fornece a contagem de clientes).


GET /customers

response payload:
{numFound: 1234, paging: {self:..., next:..., previous:...} customer: { ...} ....}


Eu gosto da maneira de agrupamento lógico de campos no sub-recurso POST.
gertas

3

Use PUT para atualizar o recurso incompleto / parcial.

Você pode aceitar o jObject como parâmetro e analisar seu valor para atualizar o recurso.

Abaixo está a função que você pode usar como referência:

public IHttpActionResult Put(int id, JObject partialObject)
{
    Dictionary<string, string> dictionaryObject = new Dictionary<string, string>();

    foreach (JProperty property in json.Properties())
    {
        dictionaryObject.Add(property.Name.ToString(), property.Value.ToString());
    }

    int id = Convert.ToInt32(dictionaryObject["id"]);
    DateTime startTime = Convert.ToDateTime(orderInsert["AppointmentDateTime"]);            
    Boolean isGroup = Convert.ToBoolean(dictionaryObject["IsGroup"]);

    //Call function to update resource
    update(id, startTime, isGroup);

    return Ok(appointmentModelList);
}

2

Em relação à sua atualização.

Acredito que o conceito de CRUD tenha causado alguma confusão em relação ao design da API. CRUD é um conceito geral de baixo nível para operações básicas executarem dados, e verbos HTTP são apenas métodos de solicitação ( criados há 21 anos ) que podem ou não ser mapeados para uma operação CRUD. De fato, tente encontrar a presença do acrônimo CRUD na especificação HTTP 1.0 / 1.1.

Um guia muito bem explicado que aplica uma convenção pragmática pode ser encontrado na documentação da API da plataforma em nuvem do Google . Ele descreve os conceitos por trás da criação de uma API baseada em recursos, que enfatiza uma grande quantidade de recursos sobre as operações e inclui os casos de uso que você está descrevendo. Embora seja apenas um design de convenção para o produto deles, acho que faz muito sentido.

O conceito básico aqui (e um que gera muita confusão) é o mapeamento entre "métodos" e verbos HTTP. Uma coisa é definir quais "operações" (métodos) sua API fará sobre quais tipos de recursos (por exemplo, obtenha uma lista de clientes ou envie um email) e outra são os verbos HTTP. Deve haver uma definição de ambos, os métodos e os verbos que você planeja usar e um mapeamento entre eles .

Ele também diz que, quando uma operação não mapeia exatamente com um método padrão ( List, Get, Create, Update, Deleteneste caso), pode-se usar "métodos personalizados", como BatchGet, que recupera vários objetos com base em vários input object id, ou SendEmail.


2

RFC 7396 : JSON Merge Patch (publicado quatro anos após a publicação da pergunta) descreve as práticas recomendadas para um PATCH em termos de regras de formato e processamento.

Em poucas palavras, você envia um HTTP PATCH para um recurso de destino com o tipo de mídia application / merge-patch + json MIME e um corpo representando apenas as partes que você deseja alterar / adicionar / remover e, em seguida, siga as regras de processamento abaixo.

Regras :

  • Se o patch de mesclagem fornecido contiver membros que não aparecem no destino, esses membros serão adicionados.

  • Se o destino contiver o membro, o valor será substituído.

  • Valores nulos no patch de mesclagem recebem um significado especial para indicar a remoção dos valores existentes no destino.

Exemplos de casos de teste que ilustram as regras acima (como visto no apêndice dessa RFC):

 ORIGINAL         PATCH           RESULT
--------------------------------------------
{"a":"b"}       {"a":"c"}       {"a":"c"}

{"a":"b"}       {"b":"c"}       {"a":"b",
                                 "b":"c"}
{"a":"b"}       {"a":null}      {}

{"a":"b",       {"a":null}      {"b":"c"}
"b":"c"}

{"a":["b"]}     {"a":"c"}       {"a":"c"}

{"a":"c"}       {"a":["b"]}     {"a":["b"]}

{"a": {         {"a": {         {"a": {
  "b": "c"}       "b": "d",       "b": "d"
}                 "c": null}      }
                }               }

{"a": [         {"a": [1]}      {"a": [1]}
  {"b":"c"}
 ]
}

["a","b"]       ["c","d"]       ["c","d"]

{"a":"b"}       ["c"]           ["c"]

{"a":"foo"}     null            null

{"a":"foo"}     "bar"           "bar"

{"e":null}      {"a":1}         {"e":null,
                                 "a":1}

[1,2]           {"a":"b",       {"a":"b"}
                 "c":null}

{}              {"a":            {"a":
                 {"bb":           {"bb":
                  {"ccc":          {}}}
                   null}}}

1

Confira http://www.odata.org/

Ele define o método MERGE, portanto, no seu caso, seria algo como isto:

MERGE /customer/123

<customer>
   <status>DISABLED</status>
</customer>

Somente a statuspropriedade é atualizada e os outros valores são preservados.


É MERGEum verbo HTTP válido?
John Saunders

3
Veja o PATCH - que é o futuro padrão HTTP e faz a mesma coisa.
Jan Algermissen

@ John Saunders Sim, é um método de extensão.
Max Toro

O FYI MERGE foi removido do OData v4. MERGE was used to do PATCH before PATCH existed. Now that we have PATCH, we no longer need MERGE. Veja docs.oasis-open.org/odata/new-in-odata/v4.0/cn01/…
tanguy_k

0

Não importa. Em termos de REST, você não pode fazer um GET, porque não é armazenável em cache, mas não importa se você usa POST ou PATCH ou PUT ou o que quer que seja, e não importa a aparência da URL. Se você estiver executando o REST, o que importa é que, quando você obtém uma representação de seu recurso no servidor, essa representação é capaz de fornecer ao cliente opções de transição de estado.

Se sua resposta GET teve transições de estado, o cliente só precisa saber como lê-las e o servidor pode alterá-las, se necessário. Aqui, uma atualização é feita usando POST, mas se ela foi alterada para PATCH ou se a URL for alterada, o cliente ainda saberá como fazer uma atualização:

{
  "customer" :
  {
  },
  "operations":
  [
    "update" : 
    {
      "method": "POST",
      "href": "https://server/customer/123/"
    }]
}

Você pode ir até a lista de parâmetros obrigatórios / opcionais para o cliente devolver a você. Depende da aplicação.

No que diz respeito às operações de negócios, esse pode ser um recurso diferente vinculado ao recurso do cliente. Se você deseja enviar um email para o cliente, talvez esse serviço seja um recurso próprio para o qual você pode POST, portanto, você pode incluir a seguinte operação no recurso do cliente:

"email":
{
  "method": "POST",
  "href": "http://server/emailservice/send?customer=1234"
}

Alguns bons vídeos e exemplos da arquitetura REST do apresentador são estes. O Stormpath usa apenas GET / POST / DELETE, o que é bom, já que o REST não tem nada a ver com as operações que você usa ou com a aparência das URLs (exceto que as GETs devem ser armazenadas em cache):

https://www.youtube.com/watch?v=pspy1H6A3FM ,
https://www.youtube.com/watch?v=5WXYw4J4QOU ,
http://docs.stormpath.com/rest/quickstart/

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.