API REST - Criação ou atualização em massa em solicitação única [fechado]

92

Vamos supor que existam dois recursos Bindere Doccom relação de associação, o que significa que Doce e Bindersão independentes. Docpode ou não pertencer a Bindere Binderpode estar vazio.

Se eu quiser criar uma API REST que permita ao usuário enviar uma coleção de Docs, EM UMA ÚNICA SOLICITAÇÃO , como a seguir:

{
  "docs": [
    {"doc_number": 1, "binder": 1}, 
    {"doc_number": 5, "binder": 8},
    {"doc_number": 6, "binder": 3}
  ]
}

E para cada doc no docs,

  • Se docexistir, atribua-o aBinder
  • Se docnão existir, crie-o e atribua-o

Estou realmente confuso sobre como isso deve ser implementado:

  • Qual método HTTP usar?
  • Qual código de resposta deve ser retornado?
  • Isso está qualificado para REST?
  • Como seria o URI? /binders/docs?
  • Tratamento de solicitação em massa, e se alguns itens gerassem um erro, mas os outros fossem processados. Qual código de resposta deve ser retornado? A operação em massa deve ser atômica?
Sam R.
fonte

Respostas:

58

Acho que você poderia usar um método POST ou PATCH para lidar com isso, pois eles normalmente projetam para isso.

  • Usar um POSTmétodo normalmente é usado para adicionar um elemento quando usado no recurso de lista, mas você também pode oferecer suporte a várias ações para esse método. Veja esta resposta: Como atualizar uma coleção de recursos REST . Você também pode oferecer suporte a diferentes formatos de representação para a entrada (se eles corresponderem a uma matriz ou a um único elemento).

    Nesse caso, não é necessário definir seu formato para descrever a atualização.

  • Usar um PATCHmétodo também é adequado, uma vez que as solicitações correspondentes correspondem a uma atualização parcial. De acordo com RFC5789 ( http://tools.ietf.org/html/rfc5789 ):

    Vários aplicativos que estendem o protocolo HTTP (Hypertext Transfer Protocol) requerem um recurso para fazer modificações parciais de recursos. O método HTTP PUT existente permite apenas a substituição completa de um documento. Esta proposta adiciona um novo método HTTP, PATCH, para modificar um recurso HTTP existente.

    Nesse caso, você deve definir seu formato para descrever a atualização parcial.

Eu acho que neste caso, POSTe PATCHsão bastante semelhantes, já que você realmente não precisa descrever a operação a ser feita para cada elemento. Eu diria que depende do formato da representação a enviar.

O caso de PUTé um pouco menos claro. Na verdade, ao usar um método PUT, você deve fornecer a lista completa. Na verdade, a representação fornecida na solicitação será em substituição ao recurso de lista um.

Você pode ter duas opções em relação aos caminhos do recurso.

  • Usando o caminho do recurso para a lista de documentos

Nesse caso, você precisa fornecer explicitamente o link dos documentos com um fichário na representação fornecida na solicitação.

Aqui está um exemplo de rota para isso /docs.

O conteúdo dessa abordagem pode ser para o método POST:

[
    { "doc_number": 1, "binder": 4, (other fields in the case of creation) },
    { "doc_number": 2, "binder": 4, (other fields in the case of creation) },
    { "doc_number": 3, "binder": 5, (other fields in the case of creation) },
    (...)
]
  • Usando o caminho de sub-recurso do elemento de ligação

Além disso, você também pode considerar aproveitar sub-rotas para descrever o link entre documentos e fichários. As dicas sobre a associação entre um documento e um fichário não precisam ser especificadas no conteúdo da solicitação.

Aqui está um exemplo de rota para isso /binder/{binderId}/docs. Nesse caso, o envio de uma lista de documentos com um método POSTou PATCHos anexará ao fichário com o identificador binderIdapós ter criado o documento se ele não existir.

O conteúdo dessa abordagem pode ser para o método POST:

[
    { "doc_number": 1, (other fields in the case of creation) },
    { "doc_number": 2, (other fields in the case of creation) },
    { "doc_number": 3, (other fields in the case of creation) },
    (...)
]

Em relação à resposta, cabe a você definir o nível de resposta e os erros a serem retornados. Vejo dois níveis: o nível de status (nível global) e o nível de carga útil (nível mais fino). Também cabe a você definir se todas as inserções / atualizações correspondentes à sua solicitação devem ser atômicas ou não.

  • Atômico

Nesse caso, você pode aproveitar o status HTTP. Se tudo correr bem, você ganha um status 200. Caso contrário, outro status, como 400se os dados fornecidos não estão corretos (por exemplo, o id do fichário não é válido) ou outro.

  • Não atômico

Nesse caso, um status 200será retornado e cabe à representação da resposta descrever o que foi feito e onde eventualmente ocorrerão erros. ElasticSearch tem um ponto de extremidade em sua API REST para atualização em massa. Isso pode lhe dar algumas idéias neste nível: http://www.elasticsearch.org/guide/en/elasticsearch/guide/current/bulk.html .

  • Assíncrono

Você também pode implementar um processamento assíncrono para lidar com os dados fornecidos. Nesse caso, o status do HTTP retornado será 202. O cliente precisa puxar um recurso adicional para ver o que acontece.

Antes de terminar, também gostaria de observar que a especificação OData aborda a questão das relações entre entidades com o recurso denominado links de navegação . Talvez você possa dar uma olhada nisso ;-)

O link a seguir também pode ajudá-lo: https://templth.wordpress.com/2014/12/15/designing-a-web-api/ .

Espero que ajude você, Thierry

Thierry Templier
fonte
Eu segui em questão. Optei por rotas planas sem um sub-recurso aninhado. Para obter todos os documentos, ligo GET /docse recupero todos os documentos em um fichário específico GET /docs?binder_id=x,. Para excluir um subconjunto dos recursos, devo chamar DELETE /docs?binder_id=xou devo chamar DELETE /docscom um {"binder_id": x}no corpo da solicitação? Você usaria PATCH /docs?binder_id=xpara uma atualização em lote ou apenas PATCH /docse passaria pares?
Andy Fusniak
34

Você provavelmente precisará usar POST ou PATCH, porque é improvável que uma única solicitação que atualize e crie vários recursos seja idempotente.

Fazer PATCH /docsé definitivamente uma opção válida. Você pode achar que usar os formatos de patch padrão é complicado para seu cenário específico. Não tenho certeza sobre isso.

Você pode usar 200. Você também pode usar 207 - Multi Status

Isso pode ser feito de forma RESTful. A chave, na minha opinião, é ter algum recurso projetado para aceitar um conjunto de documentos para atualizar / criar.

Se você usar o método PATCH, acho que sua operação deve ser atômica. ou seja, eu não usaria o código de status 207 e relataria sucessos e falhas no corpo da resposta. Se você usar a operação POST, a abordagem 207 é viável. Você terá que projetar seu próprio corpo de resposta para comunicar quais operações tiveram êxito e quais falharam. Não conheço nenhum padronizado.

Darrel Miller
fonte
Muito obrigado. Por This can be done in a RESTful wayque você quer dizer a atualização e criar deve ser feito separadamente?
Sam R.
1
@norbertpy Executar algum tipo de operação de gravação em um recurso pode fazer com que outros recursos sejam atualizados e criados a partir de uma única solicitação. REST não tem problema com isso. Minha escolha de frase foi porque algumas estruturas implementam operações em massa serializando solicitações HTTP em documentos de várias partes e, em seguida, enviando as solicitações HTTP serializadas como um lote. Acho que essa abordagem viola a restrição REST de identificação de recursos.
Darrel Miller
19

PUT ing

PUT /binders/{id}/docs Crie ou atualize e relacione um único documento a um fichário

por exemplo:

PUT /binders/1/docs HTTP/1.1
{
  "docNumber" : 1
}

PATCH ing

PATCH /docs Crie documentos se eles não existirem e relacione-os a fichários

por exemplo:

PATCH /docs HTTP/1.1
[
    { "op" : "add", "path" : "/binder/1/docs", "value" : { "doc_number" : 1 } },
    { "op" : "add", "path" : "/binder/8/docs", "value" : { "doc_number" : 8 } },
    { "op" : "add", "path" : "/binder/3/docs", "value" : { "doc_number" : 6 } }
] 

Incluirei informações adicionais mais tarde, mas, por enquanto, se você quiser, dê uma olhada em RFC 5789 , RFC 6902 e William Durand's Please. Não remende como uma entrada de blog idiota .

Mauricio morales
fonte
2
Às vezes, o cliente precisa de operação em massa e não quer se importar se o recurso está lá ou não. Como eu disse na pergunta, o cliente deseja enviar um monte de docse associá-los binders. O cliente deseja criar fichários se eles não existirem e fazer a associação se existirem. Em um único pedido em massa.
Sam R.
12

Em um projeto em que trabalhei, resolvemos esse problema implementando algo que chamamos de solicitações 'em lote'. Definimos um caminho /batchonde aceitamos json no seguinte formato:

[  
   {
      path: '/docs',
      method: 'post',
      body: {
         doc_number: 1,
         binder: 1
      }
   },
   {
      path: '/docs',
      method: 'post',
      body: {
         doc_number: 5,
         binder: 8
      }
   },
   {
      path: '/docs',
      method: 'post',
      body: {
         doc_number: 6,
         binder: 3
      }
   },
]

A resposta tem o código de status 207 (Multi-Status) e se parece com isto:

[  
   {
      path: '/docs',
      method: 'post',
      body: {
         doc_number: 1,
         binder: 1
      }
      status: 200
   },
   {
      path: '/docs',
      method: 'post',
      body: {
         error: {
            msg: 'A document with doc_number 5 already exists'
            ...
         }
      },
      status: 409
   },
   {
      path: '/docs',
      method: 'post',
      body: {
         doc_number: 6,
         binder: 3
      },
      status: 200
   },
]

Você também pode adicionar suporte para cabeçalhos nesta estrutura. Implementamos algo que se mostrou útil, que são variáveis ​​para usar entre as solicitações em um lote, o que significa que podemos usar a resposta de uma solicitação como entrada para outra.

Facebook e Google têm implementações semelhantes:
https://developers.google.com/gmail/api/guides/batch
https://developers.facebook.com/docs/graph-api/making-multiple-requests

Quando você deseja criar ou atualizar um recurso com a mesma chamada, eu usaria POST ou PUT dependendo do caso. Se o documento já existe, você deseja que todo o documento seja:

  1. Substituído pelo documento que você enviou (ou seja, as propriedades ausentes na solicitação serão removidas e já existentes substituídas)?
  2. Mesclado com o documento enviado (ou seja, as propriedades ausentes na solicitação não serão removidas e as propriedades existentes serão substituídas)?

Caso deseje o comportamento da alternativa 1 deverá usar um POST e caso deseje o comportamento da alternativa 2 deverá utilizar PUT.

http://restcookbook.com/HTTP%20Methods/put-vs-post/

Como as pessoas já sugeriram, você também pode optar pelo PATCH, mas prefiro manter a API simples e não usar verbos extras se eles não forem necessários.

David Berg
fonte
5
Curta esta resposta para a Prova de Conceito e também para os links do Google e do Facebook. Mas discorde da parte final sobre POST ou PUT. Nos 2 casos citados nesta resposta, o primeiro deve ser PUT, e o segundo, PATCH.
RayLuo
@RayLuo, você pode explicar por que precisamos do PATCH além do POST e PUT?
David Berg
2
Porque é para isso que o PATCH foi inventado. Você pode ler esta definição e ver como PUT e PATCH correspondem aos seus 2 marcadores.
RayLuo
@DavidBerg, Parece que o Google preferiu outra abordagem para processar solicitações em lote, ou seja, separar o cabeçalho e o corpo de cada sub solicitação da parte correspondente de uma solicitação principal, com um limite como --batch_xxxx. Existem algumas diferenças cruciais entre as soluções do Google e do Facebook? Além disso, sobre "usar a resposta de uma solicitação como entrada para outra", parece muito interessante, você se importaria de compartilhar mais detalhes? ou que tipo de cenário deve ser usado?
Yang