我想知道如何在REST中实现以下用例。是否有可能在不损害概念模型的情况下完成?
读取或更新单个事务范围内的多个资源。例如,从Bob的银行账户转移100美元到John的账户。
据我所知,唯一的实现方式是通过欺骗来完成。您可以将POST请求发送到与John或Bob相关联的资源,并使用单个事务执行整个操作。就我而言,这会破坏REST架构,因为您实际上是通过POST隧道传输RPC调用,而不是真正地操作单个资源。
我想知道如何在REST中实现以下用例。是否有可能在不损害概念模型的情况下完成?
读取或更新单个事务范围内的多个资源。例如,从Bob的银行账户转移100美元到John的账户。
据我所知,唯一的实现方式是通过欺骗来完成。您可以将POST请求发送到与John或Bob相关联的资源,并使用单个事务执行整个操作。就我而言,这会破坏REST架构,因为您实际上是通过POST隧道传输RPC调用,而不是真正地操作单个资源。
考虑一个 RESTful 购物篮的场景。购物篮在概念上是您的交易包装器。与将多个项目添加到购物篮中,然后提交该篮子以处理订单类似,您可以将 Bob 的帐户条目添加到交易包装器中,然后将 Bill 的帐户条目添加到包装器中。当所有组件都就位时,您可以使用所有组件 POST/PUT 交易包装器。
这个问题没有回答几个重要的情况,我认为这很遗憾,因为它在Google上的搜索排名很高,针对这些搜索词。 :-)
具体来说,一个好的属性应该是:如果您发出两次POST请求(由于某些缓存中间件出现故障),您不应该将金额转移两次。
要实现这个属性,您需要创建一个事务对象,其中可以包含您已知的所有数据,并将事务处于挂起状态。
POST /transfer/txn
{"source":"john's account", "destination":"bob's account", "amount":10}
{"id":"/transfer/txn/12345", "state":"pending", "source":...}
一旦您拥有此事务,您可以提交它,类似于:
PUT /transfer/txn/12345
{"id":"/transfer/txn/12345", "state":"committed", ...}
{"id":"/transfer/txn/12345", "state":"committed", ...}
注意此时多次放置不重要;即使对交易执行GET也将返回当前状态。特别是第二个PUT会检测到第一个PUT已经处于适当的状态,只返回它--或者,如果您尝试在将其放置在“已提交”状态后将其放置在“回滚”状态,则会收到错误消息,并返回实际提交的交易。
只要您与单个数据库或具有集成事务监视器的数据库进行通信,此机制实际上将完美地工作。您还可以为交易引入超时,甚至可以使用Expires标头来表达它们。
在REST术语中,资源是名词,可以使用CRUD(创建/读取/更新/删除)动词进行操作。由于没有"转账"动词,因此我们需要定义一个"交易"资源,可以用CRUD来操作它。下面是HTTP+POX的一个例子。第一步是创建(HTTP POST方法)一个新的空的交易:
POST /transaction
这个方法返回一个事务ID,例如“1234”,对应的URL为“/transaction/1234”。请注意,多次使用POST请求不会创建具有多个ID的相同事务,并且可以避免引入“挂起”状态。此外,POST请求并不总是幂等的(REST的要求),因此通常最好将POST数据最小化。
你可以让客户端负责生成事务ID。在这种情况下,您将使用POST /transaction/1234来创建事务“1234”,如果已经存在,则服务器将返回错误。在错误响应中,服务器可以返回未使用的ID和相应的URL。使用GET方法查询新ID并不是一个好主意,因为GET不应该改变服务器状态,而创建/保留新ID将改变服务器状态。
接下来,我们使用更新(PUT HTTP方法)所有数据来更新该事务,隐式提交:
PUT /transaction/1234
<transaction>
<from>/account/john</from>
<to>/account/bob</to>
<amount>100</amount>
</transaction>
如果之前已经通过PUT请求提交了编号为"1234"的事务,服务器将返回错误响应;否则,响应将返回OK和一个URL,指向已完成事务的页面。非常好的问题,REST通常使用类似数据库的例子进行解释,其中会存储、更新、检索和删除一些东西。很少有像这个例子一样的,其中服务器应该以某种方式处理数据。我认为罗伊·菲尔丁在他的论文中没有包含任何此类例子,毕竟他的论文是基于HTTP的。
但是他确实谈到了“表现状态转移”作为状态机,其中链接移动到下一个状态。通过这种方式,文档(表示)跟踪客户端状态,而不是服务器必须跟踪它。因此,在这种情况下,没有客户端状态,只有关于你所在链接的状态。
我一直在思考这个问题,我认为当你上传内容时,让服务器为你处理某些内容并自动创建相关资源,并给你指向它们的链接是合理的(事实上,它不需要自动创建它们:它可以告诉你这些链接,只有当你跟随它们时,它才会创建)。同时还要给你创建新的相关资源的链接 - 相关资源具有相同的URI但更长(添加后缀)。例如:
/transaction
。故障将导致多个此类资源被创建,每个资源具有不同的URI。/transaction/1234/proposed
,/transaction/1234/committed
这类似于网页的操作方式,最终网页会显示“您确定要执行此操作吗?”那个最终网页本身就是交易状态的表示形式,其中包含一个指向下一个状态的链接。不仅仅是金融交易;例如,预览后在维基百科上提交。我想在REST中的区别在于,状态序列中的每个阶段都具有显式名称(其URI)。
在实际交易/销售中,通常有不同的物理文档用于交易的不同阶段(提案、采购订单、收据等)。如果买房子,可能还会有结算等更多步骤。
另一方面,对于我来说,这感觉像是在玩弄语义;我不喜欢将动词转换为名词以使其具备RESTful特性,“因为它使用名词(URI)而不是动词(RPC调用)”。也就是说,使用名词“已提交事务资源”而不是动词“提交此事务”。我想名词化的一个优点是,您可以通过名称引用该资源,而无需以其他方式指定它(例如维护会话状态,以便知道“这个”交易是什么...)
但重要的问题是:这种方法有什么好处?即以何种方式REST风格比RPC风格更好?这种技术对于处理信息是否也有帮助,而不仅仅是存储/检索/更新/删除?我认为REST的关键优势是可扩展性;其中一方面是不需要显式地维护客户端状态(而是在资源的URI中将其隐含,并在其表示中作为链接的下一个状态)。在这个意义上,它很有帮助。也许这有助于分层/流水线?另一方面,只有一个用户会查看他们特定的交易,因此没有将其缓存以便其他人阅读的优势,HTTP才是大赢家。
我已经离开这个话题10年了。回来后,当你在谷歌搜索rest+reliable时,我无法相信你会陷入伪装成科学的宗教。混乱是神话。
我将这个广泛的问题分为三个部分:
下游服务。您开发的任何Web服务都将具有您使用的下游服务,其交易语法您别无选择,只能遵循。您应该尝试隐藏所有这些内容,使您的服务用户看不到,并确保操作的所有部分成功或失败为一组,然后将此结果返回给您的用户。你需要自己设计“事务ID”类型的事务管理。因此,需要4次调用:
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)
如果需要负载平衡,您将需要处理将操作存储在数据库中或在内存中等的问题,然后处理提交、回滚和超时。
这并不是一个轻松愉快的RESTful体验。
POST: accounts/alice, new Transfer {target:"BOB", abmount:100, currency:"CHF"}.
完成了。您不需要知道这是一个必须是原子事务等的交易。您只需将钱转账,即从A向B发送资金。
但对于极少数情况,这里有一个通用解决方案:
如果您想在定义的上下文中涉及许多资源、有许多限制并且实际上跨越了业务和实现知识之间的界限(业务与实现知识),则需要传输状态。由于REST应该是无状态的,因此作为客户端,您需要传输状态。
如果传输状态,则需要隐藏其中的信息。客户端不应知道仅由实现需要但在业务方面不相关的内部信息。如果这些信息没有业务价值,则应加密状态,并使用类似令牌、密码或其他东西的隐喻。
这样可以传递内部状态,并使用加密和签名使系统仍然安全可靠。找到客户端传递状态信息的正确抽象取决于设计和架构。
真正的解决方案:
请记住,REST是使用HTTP通信,而HTTP带有使用Cookie的概念。当人们谈论REST API和涉及多个资源或请求的工作流程和交互时,经常会忘记这些Cookie。
请记住,HTTP Cookie的维基百科中写着:
Cookie旨在成为网站记住有状态信息(例如购物车中的项目)或记录用户浏览活动(包括单击特定按钮、登录或记录用户访问的页面,追溯到几个月或几年前)的可靠机制。
因此,如果您需要传递状态,请使用Cookie。它是专门为完全相同的原因而设计的,它是HTTP,因此它与REST兼容 :)。
更好的解决方案:
如果您谈论一个客户端执行涉及多个请求的工作流程,通常会谈论协议。每种协议都带有每个潜在步骤的一组前提条件,例如在执行B之前执行步骤A。
这是自然的,但将协议暴露给客户端会使一切变得更加复杂。为了避免这种情况,只需想想我们在现实世界中进行复杂交互和事物时所做的事情…… 我们使用代理。
使用代理隐喻,您可以提供一个资源,该资源可以为您执行所有必要的步骤,并将其正在执行的任务/指令存储在其列表中(因此我们可以在代理或“代理机构”上使用POST)。
一个复杂的例子:
购买房屋:
您需要证明自己的信誉(例如提供警察记录条目),确保财务细节,使用律师和可信第三方存储资金购买实际房屋,验证房屋现在属于您,并将购买事项添加到您的税收记录中等(仅作为示例,某些步骤可能不正确或其他)。
这些步骤可能需要几天时间才能完成,有些步骤可以并行执行等。
为了做到这一点,您只需给代理任务购买房屋即可:
POST: agency.com/ { task: "buy house", target:"link:toHouse", credibilities:"IamMe"}.
完成了。机构会发回一个参考,你可以用它来查看和跟踪这个工作的状态,其他事情由机构代理自动完成。
举例来说,想象一下一个故障跟踪器。基本上,你报告了故障并使用故障 ID 来查看进展情况。你甚至可以使用一个服务来监听此资源的变化。任务完成。
在REST中,您不能使用服务器端事务。
REST的一个约束条件是:
无状态
客户端-服务器通信进一步受到约束,即不会在请求之间存储任何客户端上下文。来自任何客户端的每个请求都包含服务请求所需的所有信息,并且任何会话状态都保存在客户端中。
唯一符合REST标准的方法是创建交易重做日志并将其放入客户端状态。随着请求,客户端发送重做日志,服务器重新执行交易并
但是,也许更简单的方法是使用支持服务器端事务的服务器会话技术。
<url-base>/account/a
和<url-base>/account/b
之间转移,您可以将以下内容发布到<url-base>/transfer
。
<transfer> <from><url-base>/account/a</from> <to><url-base>/account/b</to> <amount>50</amount> </transfer>这将创建一个新的转账资源并返回转账的新URL - 例如
<url-base>/transfer/256
。