翻译自原文: 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》翻译)

【翻译】API 链接与键:为什么应该使用链接而不是键来表示 API 中的关系的更多相关文章

  1. 1.【使用EF Code-First方式和Fluent API来探讨EF中的关系】

    原文链接:http://www.c-sharpcorner.com/UploadFile/3d39b4/relationship-in-entity-framework-using-code-firs ...

  2. 原创 C++应用程序在Windows下的编译、链接:第三部分 静态链接(二)

    3.5.2动态链接库的创建 3.5.2.1动态链接库的创建流程 动态链接库的创建流程如下图所示: 在系统设计阶段,主要的设计内容包括:类结构的设计以及功能类之间的关系,动态链接库的接口.在动态链接库中 ...

  3. 使用ASP.NET Web Api构建基于REST风格的服务实战系列教程【九】——API变了,客户端怎么办?

    系列导航地址http://www.cnblogs.com/fzrain/p/3490137.html 前言 一旦我们将API发布之后,消费者就会开始使用并和其他的一些数据混在一起.然而,当新的需求出现 ...

  4. API Studio 5.1.2 版本更新:加入全局搜索、支持批量测试API测试用例、读取代码注解生成文档支持Github与码云等

    最近在EOLINKER的开发任务繁重,许久在博客园没有更新产品动态了,经过这些日子,EOLINKER又有了长足的进步,增加了更多易用的功能,比如加入全局搜索.支持批量测试API测试用例.读取代码注解生 ...

  5. Socket的用法——NIO包下SocketChannel的用法 ———————————————— 版权声明:本文为CSDN博主「茶_小哥」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/ycgslh/article/details/79604074

    服务端代码实现如下,其中包括一个静态内部类Handler来作为处理器,处理不同的操作.注意在遍历选择键集合时,没处理完一个操作,要将该请求在集合中移除./*模拟服务端-nio-Socket实现*/pu ...

  6. php extract 函数的妙用 数组键名为声明为变量,键值赋值为变量内容

    extract 函数的妙用 数组键名为声明为变量,键值赋值为变量内容 它的主要作用是将数组展开,键名作为变量名,元素值为变量值,可以说为数组的操作提供了另外一个方便的工具

  7. 驱动开发学习笔记. 0.07 Uboot链接地址 加载地址 和 链接脚本地址

    驱动开发学习笔记. 0.07 Uboot链接地址 加载地址 和 链接脚本地址 最近重新看了乾龙_Heron的<ARM 上电启动及 Uboot 代码分析>(下简称<代码分析>) ...

  8. linux笔记:链接命令,软链接和硬链接

    命令名称:ln功能:生成链接文件命令所在目录:/bin/ln用法:ln [-s] 原文件 目标文件参数:-s 创建软链接(不写此参数则生成硬链接) 软链接:类似windows中的快捷方式.它只是一个链 ...

  9. Mysql增加主键或者更改表的列为主键的sql语句

                                                                                                        ...

  10. MyBatis自动获取主键,MyBatis使用Oracle返回主键,Oracle获取主键

    MyBatis自动获取主键,MyBatis使用Oracle返回主键,Oracle获取主键 >>>>>>>>>>>>>> ...

随机推荐

  1. PHPMQTT问题一二三

    问题一:PHPMQTT作为客户端订阅超过一定数量的主题后,系统就会报错. 思路:在网上查找原因,失败: 打开调试debug = true ; 结果proc方法中报错: eof receive 问题二: ...

  2. mysql管理工具mysqladmin的使用

    1. 初始化密码 mysqladmin -uroot -p'password' password 'new-password' [root@controller3 ~]# yum -y install ...

  3. c++小练习——黑白棋

    没什么好发的,发给黑白棋水一水,如果有人能发现问题就更好了. /* Othello.cpp 黑白棋,实现随时结束并判断胜负的功能 成功运行于Visual Studio 2013 */ #include ...

  4. Python基础之数据库:2、MySQL的下载与安装、基本使用、系统服务制作

    目录 一.MySQL简介 二.安装与下载 1.下载流程 2.配置环境变量 三.主要目录介绍 四.基本使用 五.系统服务的制作 六.密码相关 1.修改管理员密码 2.忘记密码 一.MySQL简介 ​ M ...

  5. lightdm开机无法自启问题

    简述 由于我学习了 systemctl disable 服务 这条命令,然后开始皮,把 lightdm 自启动关了,然后开不开了 解决办法:重置 lightdm 服务配置 sudo dpkg-reco ...

  6. 【Spring系列】- Spring循环依赖

    Spring循环依赖 生命不息,写作不止 继续踏上学习之路,学之分享笔记 总有一天我也能像各位大佬一样 一个有梦有戏的人 @怒放吧德德 分享学习心得,欢迎指正,大家一起学习成长! 目录 Spring循 ...

  7. docker-compose + mysql8.x 主从数据库配置

    0.准备 (略过docker的安装与镜像拉取) docker / docker-compose 安装 拉取 mysql 8.x 1. master和slave的mysql配置 master: [mys ...

  8. cmd命令行ssh连接Linux服务器

    打开cmd工具 使用命令ssh连接服务器 ssh 用户名@ip地址 (不需要指定端口号,默认端口就是22) 输入密码即可

  9. [编程基础] C++多线程入门6-事件处理的需求

    原始C++标准仅支持单线程编程.新的C++标准(称为C++11或C++0x)于2011年发布.在C++11中,引入了新的线程库.因此运行本文程序需要C++至少符合C++11标准. 文章目录 6 事件处 ...

  10. (数据科学学习手札148)geopandas直接支持gdb文件写出与追加

    本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 大家好我是费老师,在我之前的某篇文章中为大家介绍 ...