翻译自原文: https://cloud.google.com/blog/products/application-development/api-design-why-you-should-use-links-not-keys-to-represent-relationships-in-apis

原文作者:Martin Nally(马丁·纳利)

原文发表时间:2019 年 5 月 11 日


在信息建模方面,如何表示两个实体之间的关系或关联是一个关键问题。用实体及其关系来描述我们在现实世界中看到的模式是一个至少可以追溯到古希腊的基本思想,也是我们今天如何看待 IT 系统中的信息的基础。

例如,关系数据库技术使用外键表示关系,外键是存储在数据库表的一行中的值,用于标识不同表或同一个表中的另一行。

表达关系在 API 中也非常重要。例如,在零售商的 API 中,信息模型的实体可能是客户、订单、目录项、购物车等。API 表示订单是针对哪个客户的,或者哪些目录项在购物车中。银行 API 表示帐户属于哪个客户或每个贷记或借记适用于哪个帐户。

API 开发人员表达关系的最常见方式是在他们公开的实体的字段中公开数据库密钥或它们的代理。但是,至少对于 Web API,该方法相对于替代方法有几个缺点:Web 链接。

由Internet 工程任务组(IETF)标准化,您可以将 Web 链接视为表示 Web 上的关系的一种方式。最著名的网络链接当然是那些出现在使用链接或锚元素表示的 HTML 网页或 HTTP 标头中的链接。但是链接也可以出现在 API 资源中,使用它们而不是外键可以显着减少 API 提供者必须单独记录并由用户学习的信息量。

链接是一个 Web 资源中的一个元素,它包括对另一个资源的引用以及两个资源之间的关系名称。对另一个实体的引用是使用称为统一资源标识符 (URI) 的特殊格式编写的,对此有IETF 标准. 该标准使用“资源”一词来表示由 URI 引用的任何实体。链接中的关系名称可以认为类似于关系数据库外键列的列名,链接中的 URI 类似于外键值。到目前为止,最有用的 URI 是可用于使用标准 Web 协议获取有关所引用资源的信息的 URI——此类 URI 称为统一资源定位器 (URL)——而迄今为止,最重要的 API 类型的 URL 是HTTP 网址。

虽然链接在 API 中并未广泛使用,但一些非常突出的 Web API 使用基于 HTTP URL 的链接来表示关系,例如Google Drive APIGitHub API。这是为什么?在这篇文章中,我将展示在实践中使用 API 外键的情况,解释它与使用链接相比的缺点,并向您展示如何将该设计转换为使用链接的设计。

使用外键表示关系

考虑流行的教学“宠物店”应用程序。该应用程序存储电子记录以跟踪宠物及其主人。宠物具有名称、种类和品种等属性。业主有姓名和地址。每只宠物都与它的主人有关系——关系的反面定义了特定主人拥有的宠物。

在典型的基于“key”的设计中​​,宠物商店应用程序的 API 提供了两个可用的资源,如下所示:

LassieJoe 之间的关系在 Lassie 的表示中使用“owner”名称/值对来表示。关系的倒数没有表达。“owner”值“98765”是外键。很可能它真的是一个数据库外键——也就是说,它是某个数据库表中某行的主键的值——但即使 API 实现对键值进行了一些转换,它仍然具有一般性外键的特征。

值“98765”对客户端的直接使用是有限的。对于最常见的用途,客户端需要使用该值组成一个 URL,并且 API 文档需要描述一个用于执行此转换的公式。这通常通过定义URI 模板来完成,如下所示:

/people/{person_id}

关系的反面——属于主人的宠物——也可以通过实现和记录以下 URI 模板之一在 API 中公开(两者之间的区别是风格问题,而不是实质问题):

/pets?owner={person_id}
/people/{person_id}/pets

以这种方式设计的 API 通常需要定义和记录许多 URI 模板。用于记录这些 API 模板的最流行语言不是 IETF 规范中定义的语言,而是OpenAPI(以前称为 Swagger)。在 3.0 版之前,OpenAPI 没有提供一种方法来指定哪些字段值可以插入哪些模板,因此还需要提供者提供的一些自然语言文档或客户端的猜测。OpenAPI 3.0 版引入了一种 称为“links”的新语法 来满足这一需求,但始终如一地使用此功能需要工作。

总而言之,虽然这种风格很常见,但它需要提供者记录,客户端学习和使用大量的 URI 模板,它们的用法没有被当前的 API 规范语言完美描述。幸运的是,有一个更好的选择。

使用链接表示关系

想象上面的资源被修改为如下所示:

主要区别在于关系是使用链接而不是外键值来表示的。在这些示例中,链接使用简单的 JSON 名称/值对来表示(请参阅下面的部分,了解在 JSON 中编写链接的其他方法的讨论)。

另请注意,宠物与其所有者的反向关系已通过将“宠物”字段添加到乔的表示中来明确表示。

将“id”更改为“self”并不是真正必要或重要的,但使用“self”来标识其属性和关系由同一 JSON 对象中的其他名称/值对指定的资源是一种常见的约定。“self”是为此目的在 IANA 注册的名称

从实现的角度来看,用链接替换所有数据库键是一个相当简单的更改——服务器将数据库外键转换为 URL,因此客户端不必这样做——但它显着简化了 API 并减少了耦合客户端和服务器。许多对第一个设计至关重要的 URI 模板不再需要,并且可以从 API 规范和文档中删除。

服务器现在可以在不影响客户端的情况下随时更改新 URL 的格式(当然,服务器必须继续遵守所有以前发布的 URL)。服务器传递给客户端的 URL 必须包含数据库中实体的主键以及一些路由信息,但是因为客户端只是将 URL 回显给服务器,并且客户端永远不需要解析 URL ,客户端不必知道 URL 的格式。这减少了客户端和服务器之间的耦合。如果服务器想向客户端强调他们不应该对 URL 格式做出假设或从中推断含义,服务器甚至可以使用 base64 或类似编码来混淆其 URL。

在上面的示例中,我在链接中使用了相对形式的 URI,例如 /people/98765。如果我以绝对形式(例如 https://pets.org/people/98765)表示 URI,对客户端可能会稍微方便一些(尽管对于这篇博客文章的格式不太方便)。客户只需要知道 IETF 规范中定义的 URI 的标准规则就可以在这两种 URI 形式之间进行转换,因此您选择使用哪种形式并不像您最初想象的那么重要。将此与前面描述的从外键转换为 URL 进行对比,这需要特定于宠物商店 API 的知识。相对 URL 对服务器实现者有一些优势,如下所述,但绝对 URL 对大多数客户端可能更方便,

简而言之,使用链接而不是外键来表达 API 中的关系减少了客户端使用 API 需要知道的信息量,并减少了客户端和服务器相互耦合的方式。

注意事项

以下是您在使用链接之前应该考虑的一些事项。

出于安全、负载平衡和其他原因,许多 API 实现在它们前面都有反向代理。一些代理喜欢重写 URL。当 API 使用外键表示关系时,唯一需要由代理重写的 URL 是请求的主 URL。在 HTTP 中,该 URL 分为地址行(第一个标题行)和主机标题。

在使用链接来表达关系的 API 中,请求和响应的标头和正文中都会有其他 URL,这些 URL 也需要重写。有几种不同的处理方法:

  1. 不要重写代理中的 URL。我尽量避免 URL 重写,但这在您的环境中可能是不可能的。

  2. 在代理中,请小心查找并映射所有 URL,无论它们出现在请求和响应的标头或正文中的任何位置。我从来没有这样做过,因为在我看来这很困难、容易出错且效率低下,但其他人可能已经这样做了。

  3. 相对地写所有的链接。除了允许代理重写 URL 之外,相对 URL 还可以更容易地在测试和生产中使用相同的代码,因为代码不必配置为知道自己的主机名。正如我在上面的示例中所示,使用带有单个前导斜杠的相对 URL 编写链接对于服务器或客户端来说几乎没有缺点,但它只允许代理更改主机名(更准确地说,URL 的部分称为计划和权威),而不是路径。根据您的 URL 的设计,如果您愿意使用没有前导斜杠的相对 URL 编写链接,您可以允许代理重写路径,但我从未这样做过,因为我认为服务器编写这些会很复杂URL 可靠。没有前导斜杠的相对 URL 对客户端来说也更难使用——他们需要使用符合标准的库而不是简单的字符串连接来处理这些 URL,并且他们需要小心理解和保留基本 URL。无论如何,使用符合标准的库来处理 URL 对客户端来说都是一种很好的做法,但许多人却不这样做。

使用链接也可能会导致您重新检查您如何进行 API 版本控制。许多 API 喜欢将版本号放在 URL 中,如下所示:

/v1/pets/12345
/v2/pets/12345
/v1/people/98765
/v2/people/98765

在这种版本控制中,可以同时以多种“格式”查看单个资源的数据——这些不是在进行编辑时按时间顺序相互替换的版本。

这与能够以不同的自然语言查看相同的 Web 文档非常相似,为此有一个Web 标准; 可惜没有类似的版本。通过为每个版本提供自己的 URL,您可以将每个版本提升到完整 Web 资源的状态。像这样的“版本 URL”并没有错,但它们不适合表达链接。如果客户端请求版本 2 格式的 Lassie,并不意味着他们也想要 Lassie 的所有者 Joe 的版本 2 格式,因此服务器无法选择将哪个版本号放入链接中。甚至可能没有适用于所有者的版本 2 格式。在链接中使用特定版本的 URL 也没有概念意义 - Lassie 不属于 Joe 的特定版本,她属于 Joe 本人。因此,即使您公开了/v1/people/98765 形式的 URL 来识别特定版本的 Joe,您还应该公开 URL /people/98765 以识别 Joe 本人并在链接中使用后者。另一种选择是仅定义 URL /people/98765 并允许客户端通过包含请求标头来请求特定版本。此标头没有标准,但将其命名为 Accept-Version 与标准标头的命名非常吻合。我个人更喜欢使用标头进行版本控制并避免使用带有版本号的 URL 的方法,但是带有版本号的 URL 很流行,我经常同时实现标头和“版本 URL”,因为两者都比争论它更容易。有关 API 版本控制的更多信息,请查看此 此标头没有标准,但将其命名为 Accept-Version 与标准标头的命名非常吻合。我个人更喜欢使用标头进行版本控制并避免使用带有版本号的 URL 的方法,但是带有版本号的 URL 很流行,我经常同时实现标头和“版本 URL”,因为两者都比争论它更容易。有关 API 版本控制的更多信息,请查看此 此标头没有标准,但将其命名为 Accept-Version 与标准标头的命名非常吻合。我个人更喜欢使用标头进行版本控制并避免使用带有版本号的 URL 的方法,但是带有版本号的 URL 很流行,我经常同时实现标头和“版本 URL”,因为两者都比争论它更容易。有关 API 版本控制的更多信息,请查看此博文

您可能仍需要记录一些 URL 模板

在大多数 Web API 中,新资源的 URL 在使用 POST 创建资源时由服务器分配。如果您使用此方法进行创建并且使用链接建立关系,则无需为这些资源的 URI 发布 URI 模板。但是,某些 API 允许客户端控制新资源的 URL。让客户端控制新资源的 URL 使得许多 API 脚本模式对客户端程序员来说更加容易,并且它还支持使用 API 将信息模型与外部信息源同步的场景。为此,HTTP 有一个特殊的方法:PUT。PUT 的意思是“如果该 URL 不存在,则在该 URL 上创建资源,否则更新它” 1. 如果您的 API 允许客户端使用 PUT 创建新实体,则您必须记录组成新 URL 的规则,可能通过在 API 规范中包含 URI 模板。您还可以通过在 POST 的正文或标头中包含类似于主键的值来允许客户端对 URL 进行部分控制。这不需要 POST 本身的 URI 模板,但客户端仍需要学习 URI 模板以利用 URI 的结果可预测性。

另一个需要记录 URL 模板的地方是 API 允许客户端在 URL 中编码查询。并非每个 API 都允许您查询其资源,但这对客户端来说可能是一个非常有用的功能,让客户端在 URL 中编码查询并使用 GET 检索结果是很自然的。以下示例说明了原因。

在上面的示例中,我们在 Joe 的表示中包含了以下名称/值对:

"pets": "/pets?owner=/people/98765"

除了标准规范中所写的内容外,客户端无需了解有关此 URL 的结构的任何信息即可使用它。这意味着客户端可以从此链接获取 Joe 的宠物列表,而无需学习任何查询语言,也无需 API 记录其 URL 格式——但前提是客户端首先在 /people/98765 上执行 GET。此外,如果宠物商店 API 记录了查询功能,则客户端可以编写相同或等效的查询 URL 来为所有者检索宠物,而无需先检索所有者——知道所有者的 URI 就足够了。也许更重要的是,客户端还可以形成以下查询,否则这些查询是不可能的:

/pets?owner=/people/98765&species=Dog
/pets?species=Dog&breed=Collie

URI 规范为此目的描述了 HTTP URL 的一部分,称为查询组件— 第一个“?”之后的 URL 部分 在第一个“#”之前。我喜欢的查询 URI 样式总是将客户端指定的查询放在 URI 的查询组件中,但也允许在 URL 的路径部分表达客户端查询。无论哪种情况,您都需要向客户描述如何编写这些 URL——您正在有效地设计和记录特定于您的 API 的查询语言。当然,您也可以允许客户端将查询放在请求正文而不是 URL 中,并使用 POST 方法而不是 GET。由于 URL 的大小存在实际限制——任何超过 4k 字节的内容都具有诱惑力——即使您还支持 GET,也支持 POST 进行查询是一种很好的做法。

因为查询是 API 中非常有用的功能,并且因为设计和实现查询语言并不容易,所以出现了GraphQL等技术。我从未使用过 GraphQL,所以我不能认可它,但您可能希望评估它作为设计和实现自己的 API 查询功能的替代方案。API 查询功能,包括 GraphQL,最好用作标准 HTTP API 的补充,用于读取和写入资源,而不是替代方案。

还有一件事……用 JSON 编写链接的最佳方式是什么?

与 HTML 不同,JSON 没有用于表达链接的内置机制。许多人对如何在 JSON 中表达链接有意见,有些人已在或多或少具有官方外观的文档中发表了他们的意见,但在撰写本文时,还没有公认的标准组织批准的标准。在上面的示例中,我使用简单的 JSON 名称/值对来表示链接——这是我的首选风格,也是 Google Drive 和 GitHub 使用的风格。您可能会遇到的另一种样式如下所示:

{
"self":"/pets/12345",
"name":"Lassie",
"links":[
{
"rel":"owner",
"href":"/people/98765"
}
]
}

我个人没有看到这种风格的优点,但它的几个变种已经达到了一定程度的受欢迎程度。

JSON 中的链接还有另一种我喜欢的样式,如下所示:

{
"self":"/pets/12345",
"name":"Lassie",
"owner":{
"self":"/people/98765"
}
}

这种风格的好处是它明确表明/people/98765是一个URL,而不仅仅是一个字符串。我从RDF/JSON中学到了这种模式。采用这种模式的一个原因是,当您必须显示嵌套在另一个资源中的一个资源的信息时,您可能无论如何都必须使用它,如下例所示,并且在任何地方使用它都会提供很好的统一性:

{
"self":"/pets?owner=/people/98765",
"type":"Collection",
"contents":[
{
"self":"/pets/12345",
"name":"Lassie",
"owner":{
"self":"/people/98765"
}
}
]
}

有关如何最好地使用 JSON 来表示数据的更多想法,请参阅非常简单的 JSON

最后,属性和关系之间有什么区别?

我想大多数人都会同意 JSON 没有用于表达链接的内置机制的说法,但是对于 JSON 有另一种看法。考虑这个 JSON:

{
"self":"/people/98765",
"shoeSize":10
}

一个常见的观点是,shoeSize 是一个属性,而不是一个关系,10 是一个值,而不是一个实体。但是,也可以合理地说,字符串“10”实际上是对第十一个整数的引用,用一种特殊的符号来编写对数字的引用,而第十一个整数本身就是一个实体。如果第十一个整数是一个非常好的实体,而字符串 '10' 只是对它的引用,那么名称/值对 "shoeSize": 10 在概念上是一个链接,即使它不使用 URI .

布尔值和字符串可以使用相同的参数,因此所有 JSON 名称/值对都可以视为链接。如果您认为这种查看 JSON 的方式是有意义的,那么使用简单的 JSON 名称/值对来链接到使用 URL 引用的实体以及使用 JSON 的数字、字符串的内置引用表示法引用的实体是很自然的、布尔值和空值。

这个论点更笼统地说,属性和关系之间没有根本区别。属性只是实体与抽象或概念实体之间的关系,例如历史上被特殊处理的数字或颜色。诚然,这是一种相当抽象的看待世界的方式——如果你给大多数人看一只黑猫,问他们看到了多少物体,他们会说一个。没有多少人会说他们看到了两个物体——一只猫和黑色——以及它们之间的关系。

Links更好

传递数据库密钥的更好的 Web API,而不是链接更难学习和更难为客户端使用。它们还通过需要更多共享知识将客户端和服务器更紧密地耦合在一起,因此它们需要编写和阅读更多文档。它们唯一的优点是,因为它们很常见,程序员已经熟悉它们并且知道如何生产和使用它们。如果您努力为您的客户提供不需要大量文档的高质量 API 并最大限度地提高客户端与服务器的独立性,请考虑在您的 Web API 中公开 URL 而不是数据库密钥。

有关 API 设计的更多信息,请阅读电子书“《Web API Design: The Missing Link》”。” (也可以参考文章 如何设计RESTful API——《Web API Design: The Missing Link》翻译)

最新文章

  1. C语言计算字符串子串出现的次数
  2. javascript判断手机浏览器版本信息
  3. away3D改造白皮书
  4. 深入了解Java程序执行顺序
  5. GHOST WIN7系统64位经典优化版 V2016年
  6. BizTalk开发系列(十七) 信封架构(Envelop)
  7. Sql server存储过程中常见游标循环用法
  8. jquery.validate.js
  9. gridview checkbox从服务器端和客户端两个方面实现全选和反选
  10. 缺少对象 WScript 问题解决方法
  11. Win10家庭版设置桌面右键更换桌面壁纸
  12. Java继承--子父类中的构造函数
  13. jQuery学习之旅 Item3 属性操作与样式操作
  14. Android 弹性布局 FlexboxLayout了解一下
  15. Ubuntu16.04安装MySQL
  16. kubernetes1.4新特性(一):支持sysctl命令
  17. Nginx, HTTPS的配置
  18. 打开和写入excel文件
  19. 【HNOI2015】菜肴制作
  20. windows下 git+tortoiseGit的使用【转】

热门文章

  1. Go语言核心36讲22
  2. 基于python的数学建模---蒙特卡洛算法
  3. SpringBoot使用@Async的总结!
  4. bugku 秋名山老司机
  5. 关于tomcat8在windows2008下高并发下有关问题的解决方案
  6. 打印菱形-java
  7. MySQL锁,锁的到底是什么?
  8. 2.10:数据加工与展示-pandas清洗、Matplotlib绘制
  9. Nginx rewrite 详解
  10. 二阶段目标检测网络-Faster RCNN 详解