如何在REST API中处理新对象属性的引入

问题描述 投票:2回答:2

我有一个公共REST API,在所有公开的资源中,公开这些:

POST my/webservice/foos/
PUT my/webservice/foos/{fooId}

两个资源都使用如下的JSON模型:

{
  "a": "a mandatory string",
  "b": "another mandatory string"
}

因此,每次用户创建新Foo或更新现有Foo时,他都必须同时指定新a和新b。到现在为止还挺好。

现在,假设一个Foo实际上比这两个属性更多,说它也有一个字符串“c”,为了这个例子。该字符串c在其可能值集合中允许null。显然,该c属性还有其他一些方法可以设置。假设它可以通过Web界面在现有的Foo上设置。

让我们考虑一个Foo,其中a =“string a”,b =“string b”,c =“string c”。我在我的REST客户端上调用了这个Foo对象上面的PUT函数,而我的Foo的新状态是:a =“new string a”,b =“new string b”,c =“string c”,这是完美的精细。

现在假设部署了REST API的更新,并且Foo的JSON模型也包括c:

{
  "a": "a mandatory string",
  "b": "another mandatory string",
  "c": "a new optional string"
}

如果,使用与以前相同的客户端,我使用与以前相同的JSON对象调用相同的PUT函数,我的Foo将变为:a =“new string a”,b =“new string b”,c = null,其中,从客户的角度来看,是非常意外的..

我完全认为引入一个非null的新属性将是API的一个重大变化,需要某种版本控制。但是你认为这个案例应该被视为一个突破性的变化吗?我应该有一个仅更新a和b的v1版本,以及更新a,b和c的v2吗?是否有任何最佳做法可以避免增加此类更改的版本号?使用PATCH是一个实用的解决方案吗?

rest http api-design
2个回答
1
投票

我完全认为引入一个非null的新属性将是API的一个重大变化,需要某种版本控制。但是你认为这个案例应该被视为一个突破性的变化吗?

仅当您的API是基于RPC的。 REST只是Web的概括,其设计方式可以轻松适应变化。 Web上的内容不断变化而不会破坏客户端。 Web如何实现这一目标?

虽然我通常同意Voice,但实际上我对这个有一点不同。在REST架构中,服务器或API应该教会客户端如何执行操作。客户端应该如何知道服务器希望在某个端点上接收什么? Swagger或类似的文档方法主要用于RPC-based APIs,但REST具有HATEOAS。这里,超文本用于驱动应用程序状态。这意味着客户将使用收到的响应来探索新的可能性。

Web中使用的传统交互流是客户端将调用它感兴趣的URI并接收响应,该响应被呈现给用户以进行进一步的操作。在预期的用户输入的情况下,服务器将返回Web表单,其中用户可以输入服务器期望的数据。这正是服务器向客户端讲授他们必须发送给服务器的方式。服务器当然会在接收时检查输入,以避免在预期数字字段上的字符串文字输入等,或验证这样的数据元素是否已经存在。这样,客户端不需要事先知道服务器可能期望的数据。如果您引入新字段,客户端将在收到响应后获知该字段。

在REST架构中可以并且可能应该采用类似的方法。要么重用HTML表单,要么使用类似halo+json的类似方法,要么定义自己的媒体类型,您应该在IANA中注册。首先建议check for already existing media-types,看看它们是否提供您所需的功能。当然,客户端需要能够根据接收的内容动态呈现表单。这可能是一项非常重要的任务,尽管这里可以将大量浏览器作为参考。

虽然HTML形成了do not support PUT, DELETE or PATCH,但这些概念仍然可以应用于REST。即单击发送按钮后,客户端通常将application/x-www-form-urlencoded表示中的所有输入发送到服务器,除非client specifies an other enctype,并且由服务器将数据转换为将进一步处理的内容,即创建新资源,启动后备流程或调用其他服务。 PUT的定义方式允许它:

  • 重新配置目标资源以反映新的媒体类型
  • 在将PUT表示保存为新资源状态之前,将其转换为与资源的格式一致的格式
  • 使用415(不支持的媒体类型)响应拒绝请求,指示目标资源仅限于其当前媒体类型。

如何在服务器端存储资源数据通常是与客户端无关的实现细节。它可以简单地存储为包含描述内容的任意键和值的地图或字典对象,或者可以映射到面向对象编程中使用的某些类。此外,它可以作为纯文本存储在文件中或作为数据库中的字节blob存储。理想情况下,服务器可以将资源的状态映射到不同的代表性媒体类型格式,这也使内容类型协商更具主导性并避免使用typed resources

服务器的整个概念教导客户端它需要什么,并且客户端能够基于所接收的数据动态地呈现内容,这允许客户端即时学习新的东西并且容易地适应变化。这实际上是REST架构的核心功能,也是主要卖点之一,为什么拥有大量不受其控制的客户端的API应该针对这样的架构。

我应该有一个仅更新a和b的v1版本,以及更新a,b和c的v2吗?

你可能会读到关于Fielding's take on versioning的内容,它简单归结为:不要(在URI中粘贴客户端可见的版本号)。基本上,如果您遵循上面概述的server teaches, client learns方法,则无论如何都不需要版本化数字,除了API所有者自己的版本更改之外。 REST体系结构中的客户端无论如何都只会收到最新版本,或者更准确地说服务器向它们公开的版本,并且能够处理它。只有服务器上的某个版本的基于RPC的客户端在服务器不分离它们收到的数据时才会适应这些更改。在这种情况下,通常可以通常切换一般命名空间以避免混淆。

是否有任何最佳做法可以避免增加此类更改的版本号?

正如整篇文章所述,服务器应该教导客户,而后者应该探索他们分析响应的机会。这涉及双方。即使你有一个服务器尊重REST架构强加的所有约束,一个解析和分析URI而不是使用链接关系名称的客户端,它希望资源返回某些类型而不是协商双方理解的表示格式或者不愿意从服务器学习,而是应用编程到客户端的带外知识将随着时间的推移而中断,因此将无法进一步与这样的服务器交互。

使用PATCH是一个实用的解决方案吗?

PATCH通常是一种被误解的HTTP方法。它类似于编程中使用的补丁,其中补丁包含应用于某些给定代码的变化,以将其转换为所需的输出。实际上应该通过HTTP完成相同的操作。客户端应该获取资源的当前表示,并计算将资源状态转换为所需状态所需的更改。通常被忽略的修补的一个方面是,需要以原子方式应用补丁。要么应用所有更改,要么都不应用。

倾向于传统修补的媒体类型是application/json-patch+json中定义的RFC 6902,它定义了一组应用于被视为JSON对象的资源的指令。通过JSON Pointers,在当前表示中要更改的各个段在JSON补丁文档中进行寻址。

RFC 7396定义了一种不同的修补方法,它定义了一种更实用的方法,用于如何将更改应用于原始资源。这由application/merge-patch+json涵盖。 JSON Patch和JSON Merge Patch之间的区别在于,后者不定义要应用于文档的操作,而是包含请求中的整个更新文档。因此,它依赖于一些固定的规则来应用。即如果请求中出现一个新字段而不是执行该字段的插入,而如果现有字段的值发生更改,则会导致该字段的更新。通过使值清零来实现删除。

关于如何修补的更全面的解释可以在William Durand的优秀博客文章Please do not patch like an idiot中找到。

在实践中,如果您对资源的状态进行了部分更新,则应使用PATCH,因为在执行某些有效性检查后,PUT被定义为用提供的内容替换整个内容。虽然PUT还提供了关于如何通过重叠资源实现部分更新的提示,但我猜这种技术并不常见。对于在版本升级时添加新字段的给定方案,我认为修补不是您应该瞄准的正确方法,而是在整个答案中尝试使用server teaches, client learns方法作为大纲。


0
投票

但是你认为这个案例应该被视为一个突破性的变化吗?

如果您提前计划,请不要。

添加扩展到您的消息模式是很好的,只要您在预先沟通时注意只能添加可选元素,并且处理规则包括Must Ignore

必须忽略规则:文档接收者必须忽略他们无法识别的有效XML文档中的任何XML属性或元素。

MUST Forward

客户端必须转发(未更改)客户端无法识别的任何输入字段(URL或FORM)。

因此,例如,如果客户端知道所需的元素A和可选元素B,但服务器也知道可选元素C,那么当客户端获取资源时,它将接收{a:..., b:..., c:...} - 它可以更改它所知道的元素,但它必须单独留下元素C,并将其包含在它发送回服务器的PUT中。

c的处理模型告诉服务器如何处理不包含它的新消息。

热门问题
推荐问题
最新问题