Padrões para lidar com operações em lote nos serviços da web REST?

170

Quais padrões de design comprovados existem para operações em lote de recursos em um serviço da web estilo REST?

Estou tentando encontrar um equilíbrio entre ideais e realidade em termos de desempenho e estabilidade. Agora temos uma API em que todas as operações são recuperadas de um recurso da lista (por exemplo: GET / user) ou em uma única instância (PUT / user / 1, DELETE / user / 22, etc.).

Existem alguns casos em que você deseja atualizar um único campo de um conjunto inteiro de objetos. Parece muito desperdício enviar a representação inteira de cada objeto para frente e para trás para atualizar o campo único.

Em uma API de estilo RPC, você pode ter um método:

/mail.do?method=markAsRead&messageIds=1,2,3,4... etc. 

Qual é o equivalente REST aqui? Ou está certo comprometer-se de vez em quando. Isso arruina o design para adicionar algumas operações específicas, onde realmente melhora o desempenho, etc.? O cliente em todos os casos agora é um navegador da Web (aplicativo javascript no lado do cliente).

Mark Renouf
fonte

Respostas:

77

Um padrão RESTful simples para lotes é usar um recurso de coleção. Por exemplo, para excluir várias mensagens de uma vez.

DELETE /mail?&id=0&id=1&id=2

É um pouco mais complicado atualizar em lote recursos parciais ou atributos de recurso. Ou seja, atualize cada atributo taggedAsRead. Basicamente, em vez de tratar o atributo como parte de cada recurso, você o trata como um depósito no qual colocar recursos. Um exemplo já foi publicado. Eu ajustei um pouco.

POST /mail?markAsRead=true
POSTDATA: ids=[0,1,2]

Basicamente, você está atualizando a lista de emails marcados como lidos.

Você também pode usar isso para atribuir vários itens à mesma categoria.

POST /mail?category=junk
POSTDATA: ids=[0,1,2]

É obviamente muito mais complicado fazer atualizações parciais em lote no estilo do iTunes (por exemplo, artist + albumTitle, mas não trackTitle). A analogia do balde começa a quebrar.

POST /mail?markAsRead=true&category=junk
POSTDATA: ids=[0,1,2]

A longo prazo, é muito mais fácil atualizar um único recurso parcial ou atributos de recurso. Basta usar um sub-recurso.

POST /mail/0/markAsRead
POSTDATA: true

Como alternativa, você pode usar recursos parametrizados. Isso é menos comum nos padrões REST, mas é permitido nas especificações URI e HTTP. Um ponto-e-vírgula divide parâmetros relacionados horizontalmente em um recurso.

Atualize vários atributos, vários recursos:

POST /mail/0;1;2/markAsRead;category
POSTDATA: markAsRead=true,category=junk

Atualize vários recursos, apenas um atributo:

POST /mail/0;1;2/markAsRead
POSTDATA: true

Atualize vários atributos, apenas um recurso:

POST /mail/0/markAsRead;category
POSTDATA: markAsRead=true,category=junk

A criatividade do RESTful é abundante.

Alex
fonte
1
Pode-se argumentar que sua exclusão deve ser realmente uma postagem, pois não está destruindo esse recurso.
Chris Nicola
6
Não é necessário. POST é um método de padrão de fábrica, é menos explícito e óbvio que PUT / DELETE / GET. A única expectativa é que o servidor decida o que fazer como resultado do POST. O POST é exatamente o que sempre foi, envio os dados do formulário e o servidor faz alguma coisa (espero que seja esperado) e me dá algumas indicações quanto ao resultado. Não somos obrigados a criar recursos com o POST, apenas optamos por isso. Posso criar facilmente um recurso com PUT, apenas tenho que definir o URL do recurso como remetente (geralmente não é o ideal).
Chris Nicola
1
@nishant, nesse caso, você provavelmente não precisará fazer referência a vários recursos no URI, mas apenas transmitir tuplas com as referências / valores no corpo da solicitação. por exemplo, POST / mail / markAsRead, CORPO: i_0_id = 0 & i_0_value = true & i_1_id = 1 & i_1_value = false & i_2_id = 2 & i_2_value = true
Alex
3
ponto e vírgula é reservado para esse fim.
23413 Alex
1
Surpreendeu-se que ninguém apontou que a atualização de vários atributos em um único recurso é bem tratada PATCH- não há necessidade de criatividade nesse caso.
LB2 2/16
25

Nem um pouco - acho que o equivalente ao REST é (ou pelo menos uma solução é) quase exatamente isso - uma interface especializada projetada acomoda uma operação exigida pelo cliente.

Lembro-me de um padrão mencionado no livro Ajax in Action, de Crane e Pascarello (um excelente livro, aliás - altamente recomendado), no qual eles ilustram a implementação de um tipo de objeto CommandQueue, cuja tarefa é enfileirar solicitações em lotes e depois publique-os no servidor periodicamente.

O objeto, se bem me lembro, basicamente apenas continha uma matriz de "comandos" - por exemplo, para estender seu exemplo, cada um um registro contendo um comando "markAsRead", um "messageId" e talvez uma referência a um retorno de chamada / manipulador função - e, de acordo com algum agendamento ou ação do usuário, o objeto de comando seria serializado e publicado no servidor, e o cliente trataria do conseqüente pós-processamento.

Por acaso não tenho os detalhes à mão, mas parece que uma fila de comandos desse tipo seria uma maneira de lidar com o seu problema; reduziria substancialmente a propriedade geral e abstraia a interface do lado do servidor de uma maneira que você possa achar mais flexível no futuro.


Atualização : Aha! Encontrei um trecho desse livro on-line, completo com exemplos de código (embora eu ainda sugira pegar o livro real!). Dê uma olhada aqui , começando com a seção 5.5.3:

É fácil de codificar, mas pode resultar em muitos bits muito pequenos de tráfego para o servidor, o que é ineficiente e potencialmente confuso. Se quisermos controlar nosso tráfego, podemos capturar essas atualizações e colocá-las na fila localmente e enviá-las ao servidor em lotes à vontade. Uma fila de atualização simples implementada em JavaScript é mostrada na listagem 5.13. [...]

A fila mantém duas matrizes. queued é uma matriz indexada numericamente, à qual novas atualizações são anexadas. sent é uma matriz associativa, contendo as atualizações enviadas ao servidor, mas que aguardam uma resposta.

Aqui estão duas funções pertinentes - uma responsável por adicionar comandos à fila ( addCommand) e uma responsável por serializar e depois enviá-las ao servidor ( fireRequest):

CommandQueue.prototype.addCommand = function(command)
{ 
    if (this.isCommand(command))
    {
        this.queue.append(command,true);
    }
}

CommandQueue.prototype.fireRequest = function()
{
    if (this.queued.length == 0)
    { 
        return; 
    }

    var data="data=";

    for (var i = 0; i < this.queued.length; i++)
    { 
        var cmd = this.queued[i]; 
        if (this.isCommand(cmd))
        {
            data += cmd.toRequestString(); 
            this.sent[cmd.id] = cmd;

            // ... and then send the contents of data in a POST request
        }
    }
}

Isso deve fazer você ir. Boa sorte!

Christian Nunciato
fonte
Obrigado. Isso é muito semelhante às minhas idéias sobre como eu iria adiante se mantivéssemos as operações em lote no cliente. O problema é o tempo de ida e volta para executar uma operação em um grande número de objetos.
21430 Mark Renouf
Hum, ok - pensei que você desejasse executar a operação em um grande número de objetos (no servidor) por meio de uma solicitação leve. Eu entendi mal?
Christian Nunciato 04/02/09
Sim, mas não vejo como esse exemplo de código executaria a operação com mais eficiência. Ele agrupa solicitações, mas ainda as envia ao servidor, uma de cada vez. Estou interpretando mal?
21430 Mark Renouf
Na verdade, ele os agrupa e os envia todos de uma vez: que o loop for em fireRequest () reúne essencialmente todos os comandos pendentes, os serializa como uma string (com .toRequestString (), por exemplo, "method = markAsRead & messageIds = 1,2,3 , 4 "), atribui essa sequência a" dados "e POSTs ao servidor.
Christian Nunciato 04/02/09
20

Embora eu ache que o @Alex está no caminho certo, conceitualmente acho que deve ser o inverso do que é sugerido.

O URL está em vigor "os recursos que estamos direcionando", portanto:

    [GET] mail/1

significa obter o registro do correio com o ID 1 e

    [PATCH] mail/1 data: mail[markAsRead]=true

significa corrigir o registro de mensagens com o ID 1. A querystring é um "filtro", filtrando os dados retornados da URL.

    [GET] mail?markAsRead=true

Então, aqui estamos solicitando todos os emails já marcados como lidos. Portanto, [PATCH] para esse caminho seria dizer "corrigir os registros marcados como verdadeiros" ... o que não é o que estamos tentando alcançar.

Portanto, um método em lote, seguindo este pensamento, deve ser:

    [PATCH] mail/?id=1,2,3 <the records we are targeting> data: mail[markAsRead]=true

é claro que não estou dizendo que isso é verdadeiro REST (que não permite manipulação de registros em lote), mas segue a lógica já existente e em uso pelo REST.

fezfox
fonte
Resposta interessante! Para o seu último exemplo, não seria mais consistente com o [GET]formato a ser feito [PATCH] mail?markAsRead=true data: [{"id": 1}, {"id": 2}, {"id": 3}](ou mesmo apenas data: {"ids": [1,2,3]})? Outro benefício dessa abordagem alternativa é que você não encontrará erros "414 Request URI too long" se estiver atualizando centenas / milhares de recursos na coleção.
Rinogo 22/08/16
@rinogo - na verdade não. Este é o ponto que eu estava fazendo. A querystring é um filtro para os registros nos quais queremos atuar (por exemplo, [GET] mail / 1 obtém o registro de email com um ID 1, enquanto [GET] mail? MarkasRead = true retorna email onde markAsRead já é verdadeiro). Não faz sentido corrigir o mesmo URL (ou seja, "corrigir os registros em que markAsRead = true") quando, na verdade, queremos corrigir registros específicos com os IDs 1,2,3, independentemente do status atual do campo markAsRead. Daí o método que descrevi. Concordo que há um problema com a atualização de muitos registros. Eu construiria um ponto final menos fortemente acoplado.
Fezfox
11

Seu idioma, " Parece muito inútil ...", para mim indica uma tentativa de otimização prematura. A menos que seja possível demonstrar que o envio de toda a representação de objetos é um grande problema de desempenho (estamos falando inaceitáveis ​​para usuários com mais de 150 ms), não faz sentido tentar criar um novo comportamento de API não padrão. Lembre-se, quanto mais simples a API, mais fácil é usar.

Para exclusões, envie o seguinte, pois o servidor não precisa saber nada sobre o estado do objeto antes que a exclusão ocorra.

DELETE /emails
POSTDATA: [{id:1},{id:2}]

O próximo pensamento é que, se um aplicativo estiver enfrentando problemas de desempenho relacionados à atualização em massa de objetos, deve-se considerar a possibilidade de dividir cada objeto em vários objetos. Dessa forma, a carga útil JSON é uma fração do tamanho.

Como exemplo, ao enviar uma resposta para atualizar os status "lido" e "arquivado" de dois emails separados, você deverá enviar o seguinte:

PUT /emails
POSTDATA: [
            {
              id:1,
              to:"[email protected]",
              from:"[email protected]",
              subject:"Try this recipe!",
              text:"1LB Pork Sausage, 1 Onion, 1T Black Pepper, 1t Salt, 1t Mustard Powder",
              read:true,
              archived:true,
              importance:2,
              labels:["Someone","Mustard"]
            },
            {
              id:2,
              to:"[email protected]",
              from:"[email protected]",
              subject:"Try this recipe (With Fix)",
              text:"1LB Pork Sausage, 1 Onion, 1T Black Pepper, 1t Salt, 1T Mustard Powder, 1t Garlic Powder",
              read:true,
              archived:false,
              importance:1,
              labels:["Someone","Mustard"]
            }
            ]

Eu dividiria os componentes mutáveis ​​do email (lidos, arquivados, importantes, marcadores) em um objeto separado, pois os outros (para, de, assunto, texto) nunca seriam atualizados.

PUT /email-statuses
POSTDATA: [
            {id:15,read:true,archived:true,importance:2,labels:["Someone","Mustard"]},
            {id:27,read:true,archived:false,importance:1,labels:["Someone","Mustard"]}
          ]

Outra abordagem a ser adotada é aproveitar o uso de um PATCH. Para indicar explicitamente quais propriedades você pretende atualizar e que todas as outras devem ser ignoradas.

PATCH /emails
POSTDATA: [
            {
              id:1,
              read:true,
              archived:true
            },
            {
              id:2,
              read:true,
              archived:false
            }
          ]

As pessoas afirmam que PATCH deve ser implementado fornecendo uma matriz de alterações contendo: ação (CRUD), caminho (URL) e alteração de valor. Isso pode ser considerado uma implementação padrão, mas se você analisar a totalidade de uma API REST, é uma ocorrência não intuitiva. Além disso, a implementação acima é como o GitHub implementou o PATCH .

Para resumir, é possível aderir aos princípios RESTful com ações em lote e ainda ter um desempenho aceitável.

justin.hughey
fonte
Concordo que PATCH faz mais sentido, o problema é que, se você tiver outro código de transição de estado que precise ser executado quando essas propriedades mudarem, será mais difícil implementá-lo como um PATCH simples. Eu não acho que o REST realmente acomode qualquer tipo de transição de estado, já que é suposto ser sem estado, ele não se importa com o que está fazendo a transição de e para, apenas com o estado atual.
BeniRose 03/10
Olá BeniRose, obrigado por adicionar um comentário. Muitas vezes me pergunto se as pessoas veem algumas dessas postagens. Fico feliz em ver o que as pessoas fazem. Os recursos relacionados à natureza "sem estado" do REST definem isso como uma preocupação com o servidor não ter que manter o estado entre as solicitações. Como tal, não está claro para mim que problema você estava descrevendo, você pode elaborar com um exemplo?
precisa saber é o seguinte
8

A API do Google Drive possui um sistema realmente interessante para resolver esse problema ( veja aqui ).

O que eles fazem é basicamente agrupar solicitações diferentes em uma Content-Type: multipart/mixedsolicitação, com cada solicitação individual individual separada por algum delimitador definido. Os cabeçalhos e o parâmetro de consulta da solicitação em lote são herdados para solicitações individuais (ou seja Authorization: Bearer some_token), a menos que sejam substituídos na solicitação individual.


Exemplo : (retirado dos documentos )

Solicitação:

POST https://www.googleapis.com/batch

Accept-Encoding: gzip
User-Agent: Google-HTTP-Java-Client/1.20.0 (gzip)
Content-Type: multipart/mixed; boundary=END_OF_PART
Content-Length: 963

--END_OF_PART
Content-Length: 337
Content-Type: application/http
content-id: 1
content-transfer-encoding: binary


POST https://www.googleapis.com/drive/v3/files/fileId/permissions?fields=id
Authorization: Bearer authorization_token
Content-Length: 70
Content-Type: application/json; charset=UTF-8


{
  "emailAddress":"[email protected]",
  "role":"writer",
  "type":"user"
}
--END_OF_PART
Content-Length: 353
Content-Type: application/http
content-id: 2
content-transfer-encoding: binary


POST https://www.googleapis.com/drive/v3/files/fileId/permissions?fields=id&sendNotificationEmail=false
Authorization: Bearer authorization_token
Content-Length: 58
Content-Type: application/json; charset=UTF-8


{
  "domain":"appsrocks.com",
   "role":"reader",
   "type":"domain"
}
--END_OF_PART--

Resposta:

HTTP/1.1 200 OK
Alt-Svc: quic=":443"; p="1"; ma=604800
Server: GSE
Alternate-Protocol: 443:quic,p=1
X-Frame-Options: SAMEORIGIN
Content-Encoding: gzip
X-XSS-Protection: 1; mode=block
Content-Type: multipart/mixed; boundary=batch_6VIxXCQbJoQ_AATxy_GgFUk
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
Date: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Vary: X-Origin
Vary: Origin
Expires: Fri, 13 Nov 2015 19:28:59 GMT

--batch_6VIxXCQbJoQ_AATxy_GgFUk
Content-Type: application/http
Content-ID: response-1


HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Date: Fri, 13 Nov 2015 19:28:59 GMT
Expires: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Content-Length: 35


{
 "id": "12218244892818058021i"
}


--batch_6VIxXCQbJoQ_AATxy_GgFUk
Content-Type: application/http
Content-ID: response-2


HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Date: Fri, 13 Nov 2015 19:28:59 GMT
Expires: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Content-Length: 35


{
 "id": "04109509152946699072k"
}


--batch_6VIxXCQbJoQ_AATxy_GgFUk--
Assessores
fonte
1

Eu ficaria tentado em uma operação como a do seu exemplo a escrever um analisador de intervalo.

Não é muito complicado criar um analisador que possa ler "messageIds = 1-3,7-9,11,12-15". Certamente aumentaria a eficiência das operações gerais que cobrem todas as mensagens e é mais escalável.


fonte
Boa observação e uma boa otimização, mas a questão era se esse estilo de solicitação poderia ser "compatível" com o conceito REST.
Mark Renouf #
Oi, sim, eu entendo. A otimização torna o conceito mais RESTful e eu não queria deixar de fora meus conselhos apenas porque estava se afastando um pouco do tópico.
1

Ótimo post. Estou procurando uma solução há alguns dias. Eu vim com uma solução de usar passar uma string de consulta com um ID de grupo separados por vírgulas, como:

DELETE /my/uri/to/delete?id=1,2,3,4,5

... passando isso para uma WHERE INcláusula no meu SQL. Funciona muito bem, mas se pergunta o que os outros pensam dessa abordagem.

Roberto
fonte
1
Eu realmente não gosto, porque meio que introduz um novo tipo, a string que você usa como uma lista em que local. Prefiro analisá-lo para um tipo específico de idioma e, em seguida, posso usar o mesmo método no da mesma maneira em várias partes diferentes do sistema.
softarn
4
Um lembrete para ter cuidado com ataques de injeção de SQL e sempre limpar seus dados e usar parâmetros de ligação ao seguir essa abordagem.
justin.hughey
2
Depende do comportamento desejado DELETE /books/delete?id=1,2,3quando o livro nº 3 não existe - o WHERE INsilenciosamente ignorará os registros, enquanto eu normalmente esperaria DELETE /books/delete?id=3404 se 3 não existir.
chbrown
3
Um problema diferente que você pode encontrar ao usar esta solução é o limite de caracteres permitido em uma string de URL. Se alguém decidir excluir 5.000 registros em massa, o navegador poderá rejeitar o URL ou o servidor HTTP (Apache, por exemplo) poderá rejeitá-lo. A regra geral (que, esperançosamente, está mudando com melhores servidores e software), é o tamanho máximo de 2 KB. Onde, com o corpo de um POST, você pode ir até 10 MB. stackoverflow.com/questions/2364840/…
justin.hughey
0

Do meu ponto de vista, acho que o Facebook tem a melhor implementação.

Uma única solicitação HTTP é feita com um parâmetro em lote e um para um token.

No lote, um json é enviado. que contém uma coleção de "solicitações". Cada solicitação possui uma propriedade de método (get / post / put / delete / etc ...) e uma propriedade relative_url (uri do terminal), além disso, os métodos post e put permitem uma propriedade "body" na qual os campos são atualizados são enviadas .

mais informações em: API de lote do Facebook

Leonardo Jauregui
fonte