Transações no REST?

147

Gostaria de saber como você implementaria o seguinte caso de uso no REST. É possível, sem comprometer o modelo conceitual?

Leia ou atualize vários recursos no escopo de uma única transação. Por exemplo, transfira US $ 100 da conta bancária de Bob para a conta de John.

Tanto quanto posso dizer, a única maneira de implementar isso é trapacear. Você pode POSTAR para o recurso associado a John ou Bob e executar toda a operação usando uma única transação. Para mim, isso interrompe a arquitetura REST, porque você essencialmente encapsula uma chamada RPC através do POST, em vez de realmente operar com recursos individuais.

Gili
fonte

Respostas:

91

Considere um cenário de carrinho de compras RESTful. Conceitualmente, o carrinho de compras é seu invólucro de transação. Da mesma forma que você pode adicionar vários itens a uma cesta de compras e enviá-la para processar o pedido, você pode adicionar a entrada da conta de Bob ao invólucro da transação e, em seguida, a entrada da conta de Bill ao invólucro. Quando todas as peças estiverem no lugar, você poderá POST / PUT o wrapper de transação com todas as peças do componente.

Darrel Miller
fonte
18
Por que TransferMoneyTransaction não seria um recurso bancário viável?
Darrel Miller
8
Se você garantir que seus pontos de extremidade se referem a substantivos, geralmente é intuitivo o que os verbos GET, PUT, POST, DELETE padrão farão com esse substantivo. O RPC permite que os pontos de extremidade sejam verbos eles mesmos e, portanto, podem entrar em conflito com os verbos HTTP e a intenção se torna confusa.
Darrel Miller
10
Por exemplo, o que acontece se você executar um HTTP DELETE no nó de extremidade UpdateXYZ? Exclui XYZ? Ele exclui a atualização ou apenas faz uma atualização e ignora a exclusão do verbo HTTP. Ao manter os verbos fora do ponto final, você remove a confusão.
Darrel Miller
5
E as transações entre vários serviços? e quando você deseja fazer um conjunto de alterações 'não relacionadas' que o serviço não expõe nenhum contêiner de transação implícito. Além disso, por que ter um tipo de transação específico quando somos movidos para transações de uso geral que não têm nenhuma relação com seus dados reais? alterar. As transações podem não corresponder tranqüilas, mas parece que as transações devem ser colocadas em camadas no topo, não relacionadas às chamadas restantes, exceto pelo fato de os cabeçalhos da solicitação conterem uma referência de transação.
meandmycode
4
@meandmycode As transações do banco de dados devem ser colocadas em camadas atrás de uma interface REST. Como alternativa, você pode expor uma transação comercial (não uma transação do banco de dados) como um recurso em si e, em seguida, é necessário executar uma ação compensatória em caso de falha.
Darrel Miller
60

Existem alguns casos importantes que não são respondidos por esta pergunta, o que eu acho muito ruim, porque ele tem uma classificação alta no Google para os termos de pesquisa :-)

Especificamente, um bom propriamente seria: Se você POSTAR duas vezes (porque algum cache está oculto no intermediário), não deverá transferir a quantidade duas vezes.

Para chegar a isso, você cria uma transação como um objeto. Isso pode conter todos os dados que você já conhece e colocar a transação em um estado pendente.

POST /transfer/txn
{"source":"john's account", "destination":"bob's account", "amount":10}

{"id":"/transfer/txn/12345", "state":"pending", "source":...}

Depois de ter essa transação, você pode confirmar, algo como:

PUT /transfer/txn/12345
{"id":"/transfer/txn/12345", "state":"committed", ...}

{"id":"/transfer/txn/12345", "state":"committed", ...}

Observe que várias colocações não importam neste momento; mesmo um GET no txn retornaria o estado atual. Especificamente, o segundo PUT detectaria que o primeiro já estava no estado apropriado e apenas o retornaria - ou, se você tentar colocá-lo no estado "reversão" depois que ele já estiver no estado "confirmado", você receberá um erro e a transação confirmada real de volta.

Desde que você fale com um único banco de dados ou com um monitor de transações integrado, esse mecanismo funcionará realmente bem. Além disso, você pode introduzir tempos limites para transações, que você pode expressar usando os cabeçalhos Expira, se quiser.

Jon Watte
fonte
Discussão interessante! Gostaria de acrescentar que a postagem inicial deve ser executada em uma etapa. Ele não pode ser adicionado posteriormente (então estamos no território do carrinho de compras e os carrinhos de compras têm muitos freios e contrapesos para impedir que causem danos ao usuário final, mesmo a legislação, as transferências bancárias não) ...
Erk
33

Em termos REST, recursos são substantivos que podem ser usados ​​com verbos CRUD (criar / ler / atualizar / excluir). Como não há verbo "transferir dinheiro", precisamos definir um recurso de "transação" que possa ser usado com o CRUD. Aqui está um exemplo em HTTP + POX. O primeiro passo é criar (método HTTP POST) uma nova transação vazia :

POST /transaction

Isso retorna um ID da transação, por exemplo, "1234" e de acordo com a URL "/ transaction / 1234". Observe que disparar esse POST várias vezes não criará a mesma transação com vários IDs e também evitará a introdução de um estado "pendente". Além disso, o POST nem sempre pode ser idempotente (um requisito REST), portanto, geralmente é uma boa prática minimizar os dados nos POSTs.

Você pode deixar a geração de um ID de transação para o cliente. Nesse caso, você POST / transaction / 1234 para criar a transação "1234" e o servidor retornará um erro se ele já existir. Na resposta de erro, o servidor pode retornar um ID não utilizado no momento com um URL apropriado. Não é uma boa ideia consultar o servidor em busca de um novo ID com um método GET, pois o GET nunca deve alterar o estado do servidor, e a criação / reserva de um novo ID alterará o estado do servidor.

Em seguida, atualizamos (método HTTP PUT) a transação com todos os dados, comprometendo-a implicitamente:

PUT /transaction/1234
<transaction>
  <from>/account/john</from>
  <to>/account/bob</to>
  <amount>100</amount>
</transaction>

Se uma transação com o ID "1234" já tiver sido PUT, o servidor fornecerá uma resposta de erro, caso contrário, uma resposta OK e um URL para visualizar a transação concluída.

NB: em / account / john, "john" deve realmente ser o número da conta exclusiva de John.

Tuckster
fonte
4
Igualar REST com CRUD é um erro grave. POST não precisa significar CREATE.
12
Erro grave? Eu sei que existem diferenças entre PUT e POST, mas há um mapeamento livre para o CRUD. "Seriamente"?
Ted Johnson
3
Sim seriamente. CRUD é uma maneira de estruturar o armazenamento de dados; O REST é uma maneira de estruturar o fluxo de dados do aplicativo. Você pode fazer CRUD no REST, mas não pode fazer REST no CRUD. Eles não são equivalentes.
Jon Watte
20

Ótima pergunta, o REST é explicado principalmente com exemplos semelhantes a bancos de dados, nos quais algo é armazenado, atualizado, recuperado e excluído. Existem alguns exemplos como este, em que o servidor deve processar os dados de alguma maneira. Não acho que Roy Fielding tenha incluído alguma em sua tese, que foi baseada em http, afinal.

Mas ele fala sobre "transferência de estado representacional" como uma máquina de estado, com links passando para o próximo estado. Dessa maneira, os documentos (as representações) controlam o estado do cliente, em vez de o servidor precisar fazê-lo. Dessa forma, não há estado do cliente, apenas o estado em que link você está.

Eu estive pensando sobre isso, e me parece razoável que, para que o servidor processe algo para você, ao fazer o upload, o servidor criará automaticamente recursos relacionados e fornecerá os links para eles (na verdade, não precisa criá-los automaticamente: ele pode apenas informar os links e apenas criá-los quando e se você os seguir - criação lenta). E também fornecer links para criar novos recursos relacionados - um recurso relacionado possui o mesmo URI, mas é mais longo (adiciona um sufixo). Por exemplo:

  1. Você faz o upload ( POST ) da representação do conceito de uma transação com todas as informações. Parece uma chamada RPC, mas está realmente criando o "recurso de transação proposto". por exemplo, URI: /transaction Glitches fará com que vários recursos sejam criados, cada um com um URI diferente.
  2. A resposta do servidor indica o URI do recurso criado, sua representação - isso inclui o link ( URI ) para criar o recurso relacionado de um novo "recurso de transação confirmado". Outros recursos relacionados são o link para excluir a transação proposta. Esses são estados na máquina de estado que o cliente pode seguir. Logicamente, eles fazem parte do recurso criado no servidor, além das informações que o cliente forneceu. por exemplo URIs: /transaction/1234/proposed, /transaction/1234/committed
  3. Você POSTA no link para criar o "recurso de transação confirmada" , que cria esse recurso, alterando o estado do servidor (os saldos das duas contas) **. Por sua natureza, esse recurso pode ser criado apenas uma vez e não pode ser atualizado. Portanto, falhas que comprometem muitas transações não podem ocorrer.
  4. Você pode obter esses dois recursos para ver qual é o estado deles. Supondo que um POST possa alterar outros recursos, a proposta agora seria sinalizada como "confirmada" (ou talvez não esteja disponível).

É semelhante à maneira como as páginas da web operam, com a página final dizendo "você tem certeza de que deseja fazer isso?" Essa página da web final é ela mesma uma representação do estado da transação, que inclui um link para ir para o próximo estado. Não apenas transações financeiras; também (por exemplo) a visualização e confirmada na wikipedia. Eu acho que a distinção no REST é que cada estágio na sequência de estados tem um nome explícito (seu URI).

Nas transações / vendas da vida real, geralmente existem documentos físicos diferentes para os diferentes estágios de uma transação (proposta, pedido, recibo etc.). Ainda mais para comprar uma casa, com liquidação etc.

OTOH É como jogar semântica para mim; Fico desconfortável com a nominalização da conversão de verbos em substantivos para torná-lo RESTful, "porque usa substantivos (URIs) em vez de verbos (chamadas RPC)". ou seja, o substantivo "recurso de transação confirmada" em vez do verbo "confirmar esta transação". Eu acho que uma vantagem da nominalização é que você pode se referir ao recurso pelo nome, em vez de precisar especificá-lo de outra maneira (como manter o estado da sessão, para que você saiba o que é "essa transação" ...)

Mas a questão importante é: quais são os benefícios dessa abordagem? ie De que maneira esse estilo REST é melhor que o estilo RPC? Uma técnica excelente para páginas da Web também é útil para processar informações, além de armazenar / recuperar / atualizar / excluir? Eu acho que o principal benefício do REST é a escalabilidade; um aspecto disso não é a necessidade de manter o estado do cliente explicitamente (mas torná-lo implícito no URI do recurso e os próximos estados como links em sua representação). Nesse sentido, ajuda. Talvez isso ajude em camadas / pipelining também? OTOH, apenas um usuário analisará sua transação específica; portanto, não há vantagem em armazená-la em cache para que outros possam lê-la, a grande vitória do http.

13ren
fonte
Você poderia explicar como "não é necessário manter o estado no cliente" ajuda a escalabilidade? Que tipo de escalabilidade? Escalabilidade em que sentido?
jhegedus
11

Se você recuar para resumir a discussão aqui, é bastante claro que o REST não é apropriado para muitas APIs, principalmente quando a interação cliente-servidor é inerentemente estável, como ocorre com transações não triviais. Por que pular todas as etapas sugeridas, tanto para o cliente quanto para o servidor, a fim de seguir pedanticamente algum princípio que não se encaixa no problema? Um princípio melhor é fornecer ao cliente a maneira mais fácil, natural e produtiva de compor com o aplicativo.

Em resumo, se você realmente está realizando muitas transações (tipos, não instâncias) em seu aplicativo, não deve criar uma API RESTful.

Peris
fonte
9
Certo, mas qual deveria ser uma alternativa no caso de arquitetura de microsserviço distribuído?
Vitamon
11

Eu me afastei deste tópico por 10 anos. Voltando, não posso acreditar na religião que se disfarça de ciência em que você mergulha quando descansa no Google + confiável. A confusão é mítica.

Eu dividiria essa pergunta ampla em três:

  • Serviços a jusante. Qualquer serviço da Web que você desenvolver terá serviços a jusante que você usar e cuja sintaxe de transação você não terá outra escolha senão seguir. Você deve tentar ocultar tudo isso dos usuários do seu serviço e garantir que todas as partes de sua operação tenham êxito ou falhem como um grupo e, em seguida, retorne esse resultado aos seus usuários.
  • Seus serviços. Os clientes desejam resultados inequívocos nas chamadas de serviço da Web, e o padrão REST usual de fazer solicitações POST, PUT ou DELETE diretamente sobre recursos substantivos me parece uma maneira pobre e facilmente aprimorada de fornecer essa certeza. Se você se preocupa com a confiabilidade, precisa identificar solicitações de ação. Esse ID pode ser um guia criado no cliente ou um valor inicial de um banco de dados relacional no servidor, não importa. Para IDs gerados pelo servidor, use uma resposta de solicitação 'preflight' para trocar o ID da ação. Se essa solicitação falhar ou tiver êxito pela metade, não há problema, o cliente apenas repete a solicitação. Os IDs não utilizados não causam danos.

    Isso é importante porque permite que todas as solicitações subsequentes sejam totalmente idempotentes, no sentido de que, se forem repetidas n vezes, retornam o mesmo resultado e não fazem mais nada acontecer. O servidor armazena todas as respostas no ID da ação e, se vir a mesma solicitação, repetirá a mesma resposta. Um tratamento mais completo do padrão está neste documento do google . O documento sugere uma implementação que, acredito (!), Siga amplamente os princípios do REST. Especialistas certamente vão me dizer como isso viola os outros. Esse padrão pode ser útil para qualquer chamada não segura ao seu serviço da web, independentemente de haver ou não transações a jusante envolvidas.
  • Integração do seu serviço em "transações" controladas por serviços upstream. No contexto de serviços da web, transações ACID completas geralmente são consideradas que não valem o esforço, mas você pode ajudar muito os consumidores de seu serviço fornecendo links de cancelamento e / ou confirmação em sua resposta de confirmação e, assim, realizar transações mediante compensação .

Sua exigência é fundamental. Não deixe que as pessoas lhe digam que sua solução não é kosher. Julgue suas arquiteturas à luz de quão bem e de maneira simples elas tratam do seu problema.

bbsimonbb
fonte
9

Você precisaria rolar seu próprio tipo de "ID da transação" do gerenciamento de TX. Seriam 4 chamadas:

http://service/transaction (some sort of tx request)
http://service/bankaccount/bob (give tx id)
http://service/bankaccount/john (give tx id)
http://service/transaction (request to commit)

Você precisaria lidar com o armazenamento das ações em um banco de dados (se a carga balanceada) ou na memória ou algo assim, manipular a confirmação, a reversão e o tempo limite.

Não é realmente um dia de descanso no parque.

TheSoftwareJedi
fonte
4
Não acho que essa seja uma ilustração particularmente boa. Você só precisa de duas etapas: Criar transação (cria uma transação no estado "pendente") e Confirmar transação (confirma se não confirmada e move o recurso para o estado confirmado ou revertido).
precisa saber é o seguinte
2

Penso que, neste caso, é totalmente aceitável quebrar a pura teoria do REST nesta situação. De qualquer forma, não acho que exista algo realmente no REST que diga que você não pode tocar em objetos dependentes em casos de negócios que exigem isso.

Eu realmente acho que não vale a pena os bastidores extras pelos quais você pularia para criar um gerenciador de transações personalizado, quando você poderia simplesmente aproveitar o banco de dados para fazer isso.

Toby Hede
fonte
2

Antes de mais nada, transferir dinheiro não é algo que você não pode fazer em uma única chamada de recurso. A ação que você deseja fazer é enviar dinheiro. Então você adiciona um recurso de transferência de dinheiro à conta do remetente.

POST: accounts/alice, new Transfer {target:"BOB", abmount:100, currency:"CHF"}.

Feito. Você não precisa saber que esta é uma transação que deve ser atômica etc. Você apenas transfere dinheiro, também conhecido como. envie dinheiro de A para B.


Mas para os casos raros aqui uma solução geral:

Se você deseja fazer algo muito complexo envolvendo muitos recursos em um contexto definido, com muitas restrições que realmente atravessam a barreira o quê versus por que (negócios versus conhecimento de implementação), é necessário transferir o estado. Como o REST deve ser sem estado, você como cliente precisa transferir o estado.

Se você transferir o estado, precisará ocultar as informações internas do cliente. O cliente não deve conhecer informações internas necessárias apenas pela implementação, mas não carrega informações relevantes em termos de negócios. Se essas informações não tiverem valor comercial, o estado deve ser criptografado e uma metáfora como token, passe ou algo precisa ser usado.

Dessa maneira, pode-se passar o estado interno e, usando a criptografia e a assinatura do sistema, ainda pode estar seguro e seguro. Encontrar a abstração certa para o cliente por que ele repassa informações de estado é algo que depende do design e da arquitetura.


A solução real:

Lembre-se de que o REST está falando HTTP e HTTP vem com o conceito de uso de cookies. Esses cookies geralmente são esquecidos quando as pessoas falam sobre a API REST, fluxos de trabalho e interações que abrangem vários recursos ou solicitações.

Lembre-se do que está escrito na Wikipedia sobre cookies HTTP:

Os cookies foram projetados para ser um mecanismo confiável para os sites lembrarem informações de estado (como itens em um carrinho de compras) ou para registrar a atividade de navegação do usuário (incluindo clicar em determinados botões, fazer login ou registrar quais páginas foram visitadas pelo usuário até o momento). há meses ou anos atrás).

Então, basicamente, se você precisar passar o estado, use um cookie. Ele foi projetado exatamente pela mesma razão, é HTTP e, portanto, é compatível com o REST por design :).


A melhor solução:

Se você fala sobre um cliente executando um fluxo de trabalho envolvendo várias solicitações, geralmente fala sobre protocolo. Toda forma de protocolo vem com um conjunto de condições prévias para cada etapa potencial, como a etapa A antes de você executar a B.

Isso é natural, mas expor o protocolo aos clientes torna tudo mais complexo. Para evitá-lo, pense no que fazemos quando precisamos fazer interações e coisas complexas no mundo real. Nós usamos um agente.

Usando a metáfora do agente, você pode fornecer um recurso que pode executar todas as etapas necessárias para você e armazenar as atribuições / instruções reais em que está atuando em sua lista (para que possamos usar o POST no agente ou em uma 'agência').

Um exemplo complexo:

Comprando uma casa:

Você precisa provar sua credibilidade (como fornecer suas anotações policiais), garantir detalhes financeiros, comprar a casa real usando um advogado e um terceiro confiável armazenando os fundos, verificar se a casa agora pertence a você e adicione os itens de compra aos seus registros fiscais etc. (apenas como exemplo, algumas etapas podem estar erradas ou o que for).

Essas etapas podem levar vários dias para serem concluídas, algumas podem ser realizadas em paralelo etc.

Para fazer isso, basta atribuir ao agente a tarefa de comprar uma casa como:

POST: agency.com/ { task: "buy house", target:"link:toHouse", credibilities:"IamMe"}.

Feito. A agência envia de volta uma referência para você que você pode usar para ver e rastrear o status desse trabalho, e o restante é feito automaticamente pelos agentes da agência.

Pense em um rastreador de erros, por exemplo. Basicamente, você relata o bug e pode usar o ID do bug para verificar o que está acontecendo. Você pode até usar um serviço para ouvir as alterações desse recurso. Missão Concluída.

Martin Kersten
fonte
1

Você não deve usar transações do lado do servidor no REST.

Uma das restrições do REST:

Sem Estado

A comunicação cliente-servidor é restringida ainda mais por nenhum contexto do cliente ser armazenado no servidor entre solicitações. Cada solicitação de qualquer cliente contém todas as informações necessárias para atender à solicitação e qualquer estado da sessão é mantido no cliente.

A única maneira RESTful é criar um log de refazer transações e colocá-lo no estado do cliente. Com as solicitações, o cliente envia o log de refazer e o servidor refaz a transação e

  1. reverte a transação, mas fornece um novo log de refazer transações (um passo adiante)
  2. ou finalmente conclua a transação.

Mas talvez seja mais simples usar uma tecnologia baseada em sessão de servidor que suporte transações do lado do servidor.

bebbo
fonte
A cotação é da entrada REST da wikipedia. Essa é a fonte real ou a wikipedia conseguiu de algum lugar? Quem pode dizer o que é o contexto do cliente e o que é o servidor?
bbsimonbb
1

Acredito que seria o caso de usar um identificador exclusivo gerado no cliente para garantir que o soluço de conexão não implique em uma duplicidade salva pela API.

Eu acho que usar um campo GUID gerado pelo cliente junto com o objeto de transferência e garantir que o mesmo GUID não fosse reinserido novamente seria uma solução mais simples para o assunto da transferência bancária.

Não conheça cenários mais complexos, como reservas de passagens aéreas múltiplas ou micro arquiteturas.

Encontrei um artigo sobre o assunto, relacionando as experiências de lidar com a atomicidade da transação nos serviços RESTful .

Eduardo Rolim
fonte
0

No caso simples (sem recursos distribuídos), você pode considerar a transação como um recurso, em que o ato de criá-la atinge o objetivo final.

Portanto, para transferir entre <url-base>/account/ae <url-base>/account/b, você pode postar o seguinte em <url-base>/transfer.

<transferência>
    <from> <url-base> / account / a </from>
    <to> <url-base> / account / b </to>
    <amount> 50 </amount>
</transfer>

Isso criaria um novo recurso de transferência e retornaria o novo URL da transferência - por exemplo <url-base>/transfer/256.

No momento da postagem bem-sucedida, a transação 'real' é realizada no servidor e o valor removido de uma conta e adicionado a outra.

Isso, no entanto, não cobre uma transação distribuída (se, digamos, 'a' for mantido em um banco atrás de um serviço e 'b' em outro banco atrás de outro serviço) - além de dizer "tente frase todos operações de maneira que não exijam transações distribuídas ".

Phasmal
fonte
2
Se você não pode "expressar todas as operações de maneira que não exija transações distribuídas", precisará realmente de uma confirmação em duas fases. A melhor idéia que eu pude encontrar para implementar a confirmação de duas fases no REST é rest.blueoxen.net/cgi-bin/wiki.pl?TwoPhaseCommit , que importante não atrapalha o espaço de nomes da URL e permite que uma confirmação de duas fases seja estratificada semântica REST limpa.
Phasmal
3
O outro problema com essa sugestão é que, se um cache soluçar e POST duas vezes, você receberá duas transferências.
precisa saber é o seguinte
É verdade que, nesse caso, você precisaria ter um processo de duas etapas - crie um recurso de "transferência" com um URL exclusivo e adicione os detalhes da transferência como parte do commit (duas partes, como mencionado nas outras respostas). Obviamente, isso poderia ser formulado como a criação de um recurso de "transação" e a adição de uma operação de "transferência".
Phasmal
-3

Eu acho que você pode incluir o TAN na URL / recurso:

  1. PUT / transação para obter o ID (por exemplo, "1")
  2. [PUT, GET, POST, o que for] / 1 / account / bob
  3. [PUT, GET, POST, o que for] / 1 / conta / fatura
  4. DELETE / transação com ID 1

Apenas uma ideia.

Até
fonte
Vejo dois problemas com essa abordagem: 1) Isso implica que você não pode acessar um recurso fora de uma transação (embora talvez isso não seja grande coisa). 2) Nenhuma das respostas até agora abordou o fato de o servidor não ser mais apátrida, embora eu suspeite que nada possa ser feito sobre isso.
Gili
Bem, / 1 / account / bob e / account / bob são apenas dois recursos diferentes. :) E RE: sem estado, implica que o recurso está sempre disponível e não depende de uma solicitação anterior. Como você solicitou transações, sim, esse não é o caso. Mas, novamente, você queria transações.
Até
1
Se um cliente precisar montar URIs, sua API não será RESTful.
22610 aehlke
1
Eu não consigo entender vocês, sério! Se você tratar uma transação como um recurso (como no exemplo acima), simplesmente pare de tratar a transação no sentido clássico e utilize-a "da maneira REST adequada", o que simplifica ainda mais a programação de processos transacionais. Você pode, por exemplo, incluir um href na transação em suas respostas para contornar o deslocamento no ambiente distribuído do lado do servidor, ainda é sem estado (é apenas um recurso, não é?) E pode implementar o mecanismo de transação real de qualquer maneira quer (e se você não tiver um banco de dados nas costas?)
Matthias Hryniszak
1
De uma forma ou de outra, se você simplesmente parar de pensar em SQL / SOAP e começar a pensar em HTTP (como o navegador), tudo se tornará simples
Matthias Hryniszak