RESTFul:状态更改操作


59

我正计划构建RESTfull API,但是有些架构问题正在脑海中产生一些问题。我想避免向客户端添加后端业务逻辑,因为当业务逻辑可以快速更改时,很难实时维护多个客户端平台的更新。

假设我们将文章作为资源(api / article),我们应该如何实现诸如发布,取消发布,激活或停用之类的操作,但又要尽量使其简单?

1)我们是否应该使用api / article / {id} / {action},因为在那里可能会发生很多后端逻辑,例如推送到远程位置或更改多个属性。可能最难的是,我们需要将所有文章数据发送回API进行更新,并且无法实现多用户工作。例如,编辑器可以发送5秒钟的旧数据,并覆盖其他记者2秒钟前所做的修复,而我无法向客户解释这一点,因为发布文章的人实际上与更新内容没有任何关系。

2)创建新资源也可以是api / article- {action} / id选项,但返回的资源将不是article- {action},但我不确定这是否合适。同样在服务器端代码文章类中,正在处理两种资源上的实际工作,并且我不确定这是否与RESTfull思想背道而驰。

任何建议都欢迎。


如果将“动作”作为RESTful URI的一部分,则这是完全合法的-如果它们声明要执行的动作/算法。这有什么错api/article?action=publish?查询参数适用于资源状态取决于您提到的“算法”(或操作)的情况。例如api/articles?sort=asc,有效
博士

1
我建议您检查一下本文,这可能会给您带来更多 RESTful解决方案的启发。
Benjol 2012年

我在api / article?action = publish上看到的问题之一是,在RESTfull应用程序中,应该发送所有文章数据以进行发布,而我更愿意这样做:api / article / 4545 / publish /,无需执行其他任何操作
Miro Svrtan

2
有关此问题及更多内容的出色资源:REST API设计-资源建模
Izhaki 2015年

@PhD:虽然在协议中完全合法,但URI通常应为名词而不是动词。URI中有动词通常是REST设计不良的标志。
Lie Ryan

Answers:


49

我发现这里描述的做法很有帮助:

那些不适合CRUD操作的操作又如何呢?

这是事情变得模糊的地方。有多种方法:

  1. 重组操作以使其看起来像资源的字段。如果操作不使用参数,则此方法有效。例如,激活 动作可以映射到布尔activated字段,并通过PATCH更新到资源。
  2. 使用RESTful原则将其视为子资源。例如,GitHub的API可以让你出演一个要点PUT /gists/:id/star取消星号标记DELETE /gists/:id/star
  3. 有时候,您确实无法将操作映射到合理的RESTful结构。例如,将多资源搜索应用于特定资源的端点并没有多大意义。在这种情况下,/search即使它不是资源, 也将是最有意义的。没关系-只需从API使用者的角度进行正确的操作,并确保已对其进行了清晰记录,以免造成混淆。

我赞成这种方法2。尽管对于API调用者来说,它看起来像笨拙的人工资源,但实际上,他们不知道服务器上正在发生什么。如果我致电POST /article/123/deactivations为商品123创建新的停用请求,则服务器不仅可以停用请求的资源,还可以实际存储我的停用请求,以便以后可以检索其状态。
JustAMartin '16

2
为什么PUT /gists/:id/starPOST /gists/:id/star呢?
Filip Bartuzi

9
@FilipBartuzi因为PUT是幂等的-也就是说,无论您用相同的参数执行动作多少次,结果始终是相同的(例如,如果您将光源从关闭切换到开启,它就会改变。如果您尝试打开再次开启,没有任何变化-已经开启)。POST不是幂等的-也就是说,每次执行某个动作时,即使使用相同的参数,该动作也会产生不同的结果(例如,如果您向某人发送一封信,那么该人会收到一封信。同一个人,他们现在有2个字母)。
拉斐尔'18

9

导致您在服务器端发生主要状态和行为更改的操作(如您描述的“发布”操作)很难在REST中进行显式建模。我经常看到的解决方案是通过数据隐式地驱动这种复杂的行为。

考虑通过在线商家公开的REST API订购商品。订购是一项复杂的操作。我们将包装和运输多种产品,并向您的帐户收费,您会收到一张收据。您可以在有限的时间内取消订单,当然还有全额退款保证,您可以将产品退回以获得退款。

代替复杂的购买操作,此类API可能允许您创建新资源(采购订单)。开始时,您可以对其进行任何修改:添加或删除产品,更改送货地址,选择其他付款方式或完全取消订单。您可以进行所有这些操作,因为您还没有购买任何东西,而只是在服务器上处理一些数据。

一旦您的采购订单完成并且您的宽限期过去,服务器将锁定您的订单以防止任何进一步的更改。仅在此时,复杂的操作序列开始,但是您不能直接控制它,只能通过先前放置在采购订单中的数据间接控制它。

根据您的描述,“发布”可以以这种方式实现。您无需公开操作,而是将您已查看并想作为新资源发布的草稿副本放置在/ publish下。这样可以保证即使发布操作本身在几个小时后完成,对草稿的任何后续更新也不会被发布。


将整个资源从未发表的文章更改为草稿的想法完全适合这种情况,但通常不适合资源上存在的所有其他操作。REST应该处理吗?也许我在滥用它,应该仅将其用作CRUD,仅此而已,例如SQL查询,我不希望查询本身包含任何逻辑。
Miro Svrtan 2012年

为什么我要问所有这些?好吧,自从一段时间以来,Web应用程序开始实现多平台化之后,我宁愿在服务器上保留很多商务逻辑,因为在iOS,Android,Web,桌面或任何其他想到的平台上更新商务逻辑变得几乎不可能做很快,我想避免在更改一些小的BL时避免向后兼容的所有问题。
Miro Svrtan 2012年

2
我认为REST可以很好地处理业务逻辑,但是它不适合暴露没有考虑REST的现有业务逻辑。这就是为什么像您所说的那样,许多公司(例如Microsoft,SAP和其他公司)通常仅通过CRUD操作公开数据。看一下开放数据协议,看看他们是如何做到的。
Ferenc Mihaly 2012年

7

我们需要将所有商品数据发送回API进行更新,并且无法实现多用户工作。例如,编辑器可以发送5秒钟的旧数据,并覆盖其他记者2秒钟前所做的修复,而我无法向客户解释这一点,因为发布文章的人实际上与更新内容没有任何关系。

无论您做什么,这种事情都是一个挑战,这与分布式源代码管理(Mercurial,git等)非常相似,并且以HTTP / ReST拼写的解决方案看起来有些相似。

假设您有两个用户Alice和Bob都在工作/articles/lunch。(为清楚起见,回复以黑体显示)

首先,爱丽丝创建文章。

PUT /articles/lunch HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0

Hey Bob, what do you want for lunch today?

301 Moved Permanently
Location: /articles/lunch/1 

服务器未创建资源,因为没有附加到请求的“版本”(假设标识符为/articles/{id}/{version}。要执行创建,将Alice重定向到将要创建的文章/版本的url。Alice的用户然后,代理将在新地址处重新应用该请求。

PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0

Hey Bob, what do you want for lunch today?

201 Created

现在,该文章已创建。接下来,鲍勃看一下这篇文章:

GET /articles/lunch HTTP/1.1
Host: example.com
Authorization: Basic Ym9iOnBhc3N3b3Jk

301 Moved Permanently
Location: /articles/lunch/1 

鲍勃看着那里:

GET /articles/lunch/1 HTTP/1.1
Host: example.com
Authorization: Basic Ym9iOnBhc3N3b3Jk

200 Ok
Content-Type: text/plain

Hey Bob, what do you want for lunch today?

他决定添加自己的更改。

PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic Ym9iOnBhc3N3b3Jk

Hey Bob, what do you want for lunch today?
Does pizza sound good to you, Alice?

301 Moved Permanently
Location: /articles/lunch/2

与爱丽丝一样,鲍勃被重定向到他将要创建新版本的位置。

PUT /articles/lunch/2 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic Ym9iOnBhc3N3b3Jk

Hey Bob, what do you want for lunch today?
Does pizza sound good to you, Alice?

201 Created

最后,爱丽丝决定自己要添加到自己的文章中:

PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0

Hey Bob, what do you want for lunch today?
I was thinking about getting Sushi.

409 Conflict
Location: /articles/lunch/3
Content-Type: text/diff

---/articles/lunch/2
+++/articles/lunch/3
@@ 1,2 1,2 @@
 Hey Bob, what do you want for lunch today?
-Does pizza sound good to you, Alice?
+I was thinking about getting Sushi.

不同于正常的重定向,而是将不同的状态代码返回给客户端409,这告诉Alice她尝试从其分支的版本已被分支。无论如何都创建了新资源(如Location标题所示),并且两者之间的差异都包含在响应主体中。爱丽丝现在知道她刚刚发出的请求需要以某种方式进行合并。


所有这些重定向都与的语义有关PUT,这需要在请求行要求的确切位置创建新资源。这样也可以节省使用的请求周期POST,但随后版本号将必须由其他魔术来编码在请求中,这对我来说似乎不太明显,但在实际的API中可能仍会首选最小化请求/响应周期。


1
在这里,Versoning并不是问题,我只是说这作为使用文章作为发布操作资源的可能问题的示例
Miro Svrtan 2012年

3

这是另一个不涉及文档内容而是涉及过渡状态的示例。(我发现版本控制-通常,每个版本都可以是一个新资源-一种简单的问题。)

假设我想通过REST公开在计算机上运行的服务,以便可以停止,启动,重新启动等等。

这里最RESTful的方法是什么?例如POST / service?command = restart?还是以“正在运行”为主体的POST / service / state?

很高兴在这里整理出最佳实践,以及REST是否适合这种情况。

其次,假设我想从服务中执行一些不影响其自身状态而是触发副作用的操作。例如,一个邮递员服务将在呼叫时生成的报告发送到一堆电子邮件地址。

GET / report可能是我自己获取报告副本的一种方式。但是,如果我们想进一步推动服务器端操作,例如我上面所说的电子邮件发送,该怎么办?或写入数据库。

这些案例围绕着资源-行动的分歧而展开,我看到了以面向REST的方式处理它们的方法,但是坦率地说,这样做似乎有点不明智。可能的关键问题是,REST API是否通常应支持副作用。


2

REST是面向数据的,因此资源最适合作为“事物”而不是动作。http方法的隐式语义;GET,PUT,DELETE等用于增强方向。POST当然是例外。

资源可以是数据的混合,即 文章内容;和元数据即 发布,锁定,修订。切片数据还有许多其他可能的方法,但是必须先确定数据流的样子,然后才能确定最佳数据流(如果有的话)。例如,如TokenMacGuy所建议的那样,修订可能是文章下自己的资源。

关于实现,我可能会做类似TockenMacGuy建议的事情。我还将在文章(而不是修订)上添加元数据字段,例如“锁定”和“发布”。


1

不要认为它直接操纵了文章的状态。相反,您要输入变更单,要求创建商品。

您可以将放入变更单的过程建模为创建新的变更单资源(POST)。有很多优点。例如,您可以指定发布文章的未来日期和时间,作为变更单的一部分,并使服务器担心如何实现。

如果发布不是一个即时过程,则无需等待它完成就可以返回客户端。您只需要确认已创建变更单并返回变更单ID。然后,您可以使用与该变更单相对应的URL来共享变更单的状态。

对我来说,一个关键的见解是认识到这种变更单的隐喻只是描述面向对象编程的另一种方式。我们称资源为对象,而不是资源。我们将其称为消息,而不是变更单。在OO中从A到B发送消息的一种方法是让A在B上调用方法。另一种方法是,让A创建一个新的对象M,特别是当A和B在不同的计算机上时。将其发送给B。REST只是简单地使该过程正式化。


实际上,我认为这比面向对象更接近Actor模型。将REST请求视为消息(它们是消息)使它们非常整齐地与事件源管理状态保持一致。REST沿CQRS线巧妙地划分了其交互。GET消息是Query,POST,PUT,PATCH,DELETE属于Command区域。
WillD

0

如果我对您的理解正确,我认为您拥有的更多的是“业务规则”确定问题而不是技术问题。

可以通过引入授权级别(高级用户可以覆盖初级用户的版本)来解决文章可以被覆盖的事实,还可以通过引入版本以及捕获文章状态的列(例如“开发中”,“最终”)来解决。等),您可以克服这一点。您还可以使用户能够通过提交时间和版本号来选择给定的版本。

在上述所有情况下,您的服务都需要实现您设置的业务规则。因此,您可以使用以下参数调用该服务:用户ID,文章,版本,操作(其中版本是可选的,再次取决于您的业务规则)。


我不相信这是一个业务规则,但严格来说是一项技术规则。添加版本的想法是帮助您覆盖规则的好方法,但是仍然无法解决更新内容和发布内容无关的情况。
米罗·斯弗坦
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.