第四章 资源规模和层级结构

本章涵盖:

  • 什么是资源布局
  • 资源关系的各种类型(引用、多对多等)
  • 实体关系图是如何描述资源布局的
  • 如何选择资源之间的正确关系
  • 需要避免的资源布局反模式

正如我们在第1章中学到的,将我们的注意力从操作转向资源使我们能够通过利用简单的模式更轻松、更快速地熟悉API。例如,REST提供了一组标准动词,我们可以将其应用于许多资源,这意味着对于我们了解的每个资源,我们还可以掌握对该资源执行的五种不同操作(create、get、list、delete和update)。

尽管这很有价值,但这意味着我们需要更加慎重的定义API资源。选择正确的资源的关键部分是了解它们将来如何相互关联。在本章中,我们将探讨如何布局API中的各种资源、可用的选项以及选择正确的方式将资源彼此关联。此外,我们将讨论在考虑如何布局API中的资源时要避免的一些反模式(anti-patterns)(不应该做的事情)。让我们从具体了解资源布局的含义开始。

4.1 什么是资源布局

当我们谈论资源布局时(resource layout),通常指的是API中资源(或“事物”)的排列方式,定义这些资源的字段,以及这些资源如何通过这些字段相互关联。换句话说,资源布局是特定API设计的实体(资源)关系模型。例如,如果我们要构建一个聊天室的API,资源布局是指我们可能选择创建ChatRoom资源以及与ChatRoom有某种相关的User资源。我们感兴趣的是User和ChatRoom资源之间的关联方式。如果您曾经设计过具有各种表的关系数据库,这应该感觉很熟悉:您设计的数据库模式在本质上与API的表示方式非常相似。

虽然可能会说关系是唯一重要的事情,但实际上情况要复杂一些。虽然关系本身是对最终资源布局唯一有直接影响的因素,但还有许多其他因素间接影响布局。一个明显的例子是资源选择本身:如果我们选择不使用User资源,而是坚持使用按名称列出用户的简单列表(members: string[]),那么就没有其他资源可以布局,问题就被完全回避了。

正如名称所示,API的资源布局在视觉上可能最容易理解,即将它看作由线连接在一起的方框。例如,图4.1显示了我们可能如何考虑聊天室示例,其中有许多用户和许多聊天室彼此相互连接。

图4.1 一个有很多成员的聊天室

如果我们正在构建一个在线购物API(例如,类似于亚马逊),我们可能会存储User资源以及他们的各种地址(用于运送在线购买的物品)和付款方式。此外,付款方式本身可能会引用付款方式的账单地址。这个布局如图4.2所示。

图4.2 用户,付款方式和地址之间有不同的对应关系

简而言之,要记住资源布局是一个广泛的概念,涵盖了所有我们为API定义的资源。最重要的是这些资源如何相互交互和关联。在下一节中,我们将简要介绍不同类型的关系以及每种类型可能提供的交互(和限制)。

4.1.1 资源关系的类型

在考虑资源布局时,我们必须考虑资源关系的多种方式。重要的是要注意,我们将讨论的所有关系都是双向的,这意味着即使关系看起来是单向的(例如,一条消息指向一个用户作为作者),反向关系仍然存在,即使它没有明确定义(例如,一个用户可以是多条不同消息的作者)。让我们直接开始看最常见的关系形式:引用。

引用关系

两个资源相互关联的最简单方式是通过简单的引用。这意味着一个资源引用或指向另一个资源。例如,在聊天室API中,我们可能有组成聊天室内容的Message资源。在这种情况下,每条消息显然会由单个用户撰写,并存储在消息的作者字段中。这将导致消息和用户之间存在简单的引用关系:消息有一个指向特定用户的作者字段,如图4.3所示。

图4.3 一个Message资源包含了对其作者(User资源)的引用

这种引用关系有时被称为外键(foreign key)关系,因为每个Message资源将指向一个确切的User资源作为作者。但正如前面提到的,一个User资源显然可以与许多不同的消息相关联。因此,这也可以被视为一对多关系,其中用户可能写多条消息,但每条消息始终有一个用户作为作者。

多对多关系

就像引用关系的一种更复杂的版本,一对多关系代表了每个资源指向另一个资源的多个实例的情况。例如,如果我们有一个用于组群对话的ChatRoom资源,这显然会包含许多个体用户作为成员。但是,每个用户也可以是多个不同聊天室的成员。在这种情况下,ChatRoom资源与用户之间存在一对多关系。ChatRoom指向很多User资源作为房间的成员,并且用户能够指向多个聊天室。

关于这种关系的工作原理的具体机制将在未来进行探讨(我们将在第三部分中看到这些),但这些多对多关系在API中非常常见,并且有几种不同的选项可以表示它们,每种选项都有其自己的优点和缺点。

自引用关系

在这一点上,我们可以讨论一种可能听起来奇怪但实际上只是引用的另一个特殊版本:自引用(self-reference)。顾名思义,在这种关系中,一个资源指向与其自身相同类型的另一个资源,因此"self"指的是类型而不是资源本身。实际上,这与普通的引用关系完全相同;但是,由于其典型的可视化表示(其中一个箭头指向资源本身),我们将其视为不同的类型,如图4.4所示。

图4.4 一个Employee资源指向其他Employee资源代表其经理和助理

也许你会想知道为什么一个资源会指向与其自身相同类型的另一个资源。这是一个合理的问题,但实际上这种类型的关系出现的频率远远超出你的预期。自引用在层级结构中经常出现,在这种关系中,资源是树中的一个节点,或者在网络式API中,数据可以表示为有向图(例如社交网络)。

例如,想象一个用于存储公司目录的API。在这种情况下,员工相互指向以跟踪彼此的报告关系(例如,员工1向员工2报告)。该API还可能具有进一步的自引用,用于特殊关系(例如,员工1的助手是员工2)。在这些情况中,我们可以使用自引用来建模资源布局。

层级结构

最后,我们需要讨论一种非常特殊的关系,这是对标准引用关系的另一种理解:层级结构(hierarchies)。层级结构有点像一个资源指向另一个资源,但该指针通常指向上方,并且意味着不止一个资源指向另一个资源。与典型的引用关系不同,层级结构还倾向于反映资源之间的包含或所有权关系,可能最好使用计算机上的文件夹术语来解释。计算机上的文件夹(对于Linux用户来说是目录)包含许多文件,从这个意义上说,这些文件属于文件夹。文件夹还可以包含其他文件夹,有时会无限循环。

这可能看起来无害,但这种特殊的关系暗示了一些重要的属性和行为。例如,当你在计算机上删除一个文件夹时,通常所有包含在内的文件(和其他文件夹)也会被删除。或者,如果你被授予对特定文件夹的访问权限,这通常意味着对其中的文件(和其他文件夹)也有访问权限。这些相同的行为已经成为类似资源的预期行为。

在我们深入讨论之前,让我们看看层级结构是什么样子。实际上,在前几章中我们已经使用了层级结构的例子,因此我们不应该感到惊讶。例如,我们已经讨论过由许多消息资源组成的ChatRoom资源。在这种情况下,存在ChatRooms包含或拥有Messages的隐含层级关系,如图4.5所示。

图4.5 ChatRoom资源与Message资源的层级关系表明了一种从属关系

正如你可以想象到,这种结构通常暗示着有权限访问ChatRoom资源也将获得Message资源的访问权限。此外,通常假定删除ChatRoom资源会根据父子层级关系级联到Message资源。在某些情况下,这种级联效应是巨大的好处(例如,我们很高兴能够在计算机上删除整个文件夹,而不必首先删除里面的每个单独文件)。在其他情况下,级联行为可能会非常棘手(例如,我们可能认为我们正在授予对文件夹的访问权限,而实际上我们正在授予对该文件夹内所有文件的访问权限,包括子文件夹)。

总体而言,层级结构是复杂的,会引发许多棘手的问题。例如,资源可以更改父级吗?换句话说,我们可以将Message资源从一个ChatRoom资源移动到另一个ChatRoom资源吗?(通常,这是个坏主意)我们将在第4.2节中更深入地探讨层级结构及其缺点和优势。

4.1.2 实体关系图

在本章中,你可能已经看到了一些有趣的符号,它们出现在连接不同资源的线的末端。虽然我们没有时间深入研究UML(统一建模语言),但我们至少可以看一下其中一些箭头并解释它们的工作原理。

简而言之,相比于使用任意箭头从一个资源指向另一个资源,我们可以使箭头末端传达有关资源关系的重要信息。更具体地说,每个箭头末端可以告诉我们在另一端可能有多少资源。例如,图4.6显示学校有很多学生,但每个学生只上一所学校。此外,每个学生上很多课,每个班级包含很多学生。

图4.6 展示学校,学生和课程的实体关系图示例

虽然这两个符号显然是最常见的,但你可能偶尔会看到其他符号。这是为了涵盖资源数量为零的特殊情况(非必需资源)。例如,从技术上讲,一个班级可能由零个学生组成(反之亦然)。因此真实关系可能看起来更像图4.7。

图4.7 更新后的关系图表明一个学生可能不会参加任何课程,以及一门课程可能并没有学生选择

有时候理解这种类型的符号可能有点困难,因此澄清应该如何阅读每个连接器的方向可能有所帮助。阅读这些图表的最佳方法是从一个资源开始,沿着线走,然后查看线末端的连接器,最后到达另一个资源。重要的是要记住,您会跳过触及您起始资源的连接器符号。为了更清晰,图4.8显示了学校和学生资源之间的连接分成两个单独的图表,连接的方向写在线旁边。

图4.8 通过学校和学生资源说明如何阅读实体关系

正如我们所看到的,一个学校有很多学生,而学生只有一个学校。为简单起见,我们将这两者合并到一个单独的图表和一条连接两者的线中,就像我们在之前的示例中看到的那样。

现在我们已经很好地掌握了如何阅读这些图表的方法,让我们开始更重要的工作,即通过选择资源之间的正确关系来建模我们的API。

4.2 选择正确的资源关系

正如我们在第4.1节中所学的,选择正确类型的资源关系通常取决于我们首先选择的资源,因此我们将在本节中将这两者联系在一起。让我们首先看一个重要的问题:我们是否需要资源关系?

4.2.1 你是否真的需要某种资源关系?

在构建API时,选择了对我们重要的事物或资源之后,下一步是决定这些资源之间的关系。通常人们会像规范化数据库模式一样连接所有可能需要连接的东西,以构建一个丰富的、互相关联的资源网络(通过各种指针和引用)。虽然这肯定会导致一个非常丰富和具有描述性的API,但随着API接收越来越多的流量并存储越来越多的数据,这也变得难以维持,并且可能出现严重的性能下降。

为了理解我们的意思,让我们想象一个简单的API,用户可以在其中关注彼此(例如,类似Instagram或Twitter的东西)。在这种情况下,User资源具有一个多对多的自引用,其中一个用户关注许多其他用户,每个用户可能有许多关注者,如图4.9所示。

图4.9 一个自引用的User资源,表示用户可以关注其他用户

这可能看起来是无害的,但当用户拥有数百万个关注者(并且用户关注数百万其他用户)时,情况可能变得相当混乱,因为这些类型的关系会导致对单一资源的更改影响数百万其他相关资源的情况。例如,如果某个著名人物删除其Instagram帐户,可能需要删除或更新数百万条记录(取决于底层数据库模式)。

这并不是说您应该以任何代价都要避免所有资源关系。相反,重要的是要在早期权衡任何给定关系的长期成本。换句话说,资源布局(和关系)并不总是没有成本的。就像在签署文件之前了解您的抵押贷款可能会具体花费多少钱一样重要,同样在设计过程中了解给定API设计的真实成本也很重要,而不是在设计的最后阶段了解。尽管在真正需要维护资源关系的场景中(例如前面的例子,存储关注者关系似乎非常重要)总会有办法缓解性能下降的问题,但谨慎定义API中的引用关系仍然是明智的。

换句话说,引用关系应该是有目的的,并且对于期望的行为而言是基本的。换句话说,这些关系永远不应该是偶然发生的、可选的,或者未来可能会需要的东西。相反,任何引用关系都应该是API实现其主要目标所必不可少的东西。

例如,像Twitter、Facebook和Instagram这样的服务是建立在用户相互关注的关系基础上的。用户之间的自引用关系对于这些服务的目标确实是至关重要的。毕竟,没有这种关系,Instagram将变成一个简单的照片存储应用程序。将其与直接消息服务进行比较:我们可以看到,尽管这种API在用户之间以聊天形式表现出某种关系,但它们对应用程序的重要性并不相同。例如,在聊天中涉及的两个用户之间建立关系很重要,但没有必要对所有潜在关系建立一个集合。

4.2.2 引用数据还是內联数据

假设某种资源关系对于您的API的行为和功能至关重要,那么我们就必须探讨并回答一些重要的问题。首先,我们需要探讨是否应该在API中内联数据(即在资源内存储副本)还是依赖引用(即仅保留对正式数据的指针)。让我们通过聊天室的案例展开说明。

假设每个聊天室必须有一个单一的管理员用户。我们有两个选择:我们可以通过引用字段(例如adminUserId)指向管理员用户,或者我们可以内联此用户的数据,并将其表示为ChatRoom资源的一部分。图4.10和4.11分别展示了两种情况。

图4.10 使用adminUserId指向管理员用户

图4.11 将管理员用户数据內联至聊天室资源

在第一个图表(图4.10)中,我们可以看到ChatRoom资源在指定管理员时指向User资源。另一方面,在第二个图表(图4.11)中,我们可以看到adminUser字段实际上包含有关管理员的信息。这引出了一个显而易见的问题:哪种方法更好?事实证明,答案取决于您提出的问题或执行的操作。

如果您想查看管理员的姓名,使用内联数据会更容易。为了了解原因,让我们看看在不同情况下为了获取这些信息我们需要做什么。

代码4.1 两种情况下获取管理员的方法

function getAdminUserNameInline(chatRoomId: string): string {
    let chatRoomInline = GetChatRoomInline(chatRoomId);
    return chatRoomInline.adminUser.name;
}

function getAdminUserNameReference(chatRoomId: string): string {
    let chatRoomRef = GetChatRoomReference(chatRoomId);
    // 对于引用的情况,如果我们想要获取有关管理员的更多信息,我们需要进行第二次查找。
    let adminUser = GetAdminUser(chatRoomRef.adminUserId);
    return adminUser.name;
}

这两个函数中最明显的区别是需要网络响应的实际API调用数量。在第一种情况中,数据以内联方式返回,我们只需要一个API调用即可检索所有相关信息。而在第二种情况中,我们首先必须检索ChatRoom资源,然后才知道我们感兴趣的用户是谁。之后,我们必须检索User资源以获取姓名。

这是否意味着最好总是将数据内联?并非完全如此。这是一个例子,我们需要两倍的API调用次数才能获取我们感兴趣的信息。但如果我们并不经常关心那些信息呢?如果是这样,那么每当有人请求ChatRoom资源时,我们都会发送大量字节,而大部分字节却被无视了。换句话说,每当有人请求ChatRoom资源时,我们还要告诉他们该房间管理员的User资源的所有信息。

这可能看起来并不是什么大问题,但如果每个用户也将他们所有的朋友(其他User资源)以内联方式存储呢?在这种情况下,将聊天室的管理员与之一起返回实际上可能导致大量额外的数据。此外,当生成所有这些额外的内联数据时,通常需要额外的计算工作(通常来自底层需要连接数据的数据库查询),而这种数据可能以不可预测的方式迅速增长。那么我们该怎么办呢?

不幸的是,这将取决于您的API正在做什么,因此这是一个判断性决策(主观决策)。一个很好的经验法则是优化常见情况,而不影响高级情况的可行性。这意味着我们需要考虑相关资源是什么,它现在有多大,以及它可能会变得多大,并决定在响应中包含管理员信息有多重要。在这种情况下,典型用户可能并不经常查找他们聊天室的管理员是谁,而是更专注于向朋友发送消息。因此,内联这些数据可能并不是非常重要。另一方面,User资源可能非常小,因此如果API经常使用这些数据,内联这些数据可能并不是一个很大的问题。

话虽如此,我们还没有涉及到另一种重要的关系类型:层级结构。让我们看看应该何时选择层级结构关系。

4.2.3 层级结构

正如我们之前所了解的,层级关系是一种非常特殊的引用关系,是父资源与子资源之间的关系,而不是两个一般相关的资源。这种关系的最大区别在于操作的级联效应以及从父级到子级的行为和属性的继承。例如,通常删除父资源会意味着删除子资源。同样,对父资源的访问(或无访问权限)通常意味着对子资源的相同级别的访问。这种关系的独特而潜在有价值的特性意味着它具有很大的潜力,无论是积极的还是消极的。

我们如何决定何时将资源安排在层级关系而不是简单关系中呢?假设我们已经决定对某种特定关系的模拟是构建某个API的基础(参见第4.2.1节),我们实际上可以依赖模拟的行为作为一种关系是否合适的重要指标。

例如,当我们删除一个聊天室时,我们几乎肯定也希望删除属于该聊天室的Message资源。此外,如果有人被授予访问ChatRoom资源的权限,如果他们没有权限查看与该聊天室相关联的Message资源,那么这似乎并不合理。这两个指标意味着我们几乎肯定要将这种关系建模为一个适当的层级结构。

同样,还有一些迹象表明你也不应该使用层级关系。由于子资源只能有一个父资源,那么显然已经有一个父资源的资源不能再有另一个父资源。换句话说,如果您考虑的是一对多的关系,那么层级结构肯定不是一个好的选择。例如,Message资源应该始终属于一个单一的聊天室。如果我们想将单个Message资源与许多ChatRoom资源相关联,那么层级结构可能不是正确的模型。

这并不是说资源只能指向一个资源。毕竟,大多数资源都将是另一个资源的子资源,仍然可能被其他资源引用。例如,想象一下ChatRoom资源属于公司(这个示例的新资源类型)。公司可能有保留策略(由RetentionPolicy资源表示),指定消息在被删除之前要存留多长时间。这些保留策略将是单个公司的子资源,但可能被该公司中的任何ChatRoom资源引用。如果这让您感到困惑,请查看图4.12中的资源布局图。

图4.12 将公司范围的保留策略应用于聊天室的资源布局图

正如您在这里所看到的,User、ChatRoom和RetentionPolicy资源都是Company资源的子资源。同样,Message资源是ChatRoom资源的子资源。但是RetentionPolicy是可重用的,并且可以应用于许多不同的聊天室。

希望在这一点上,您已经对何时依赖引用与层级结构(以及何时内联信息而不是使用引用)有了一个很好的了解。但在探讨这些主题之后,我们在设计API时往往会陷入一些条件反射的反应。让我们花一点时间看看一些资源布局的反模式以及如何避免它们。

4.3 反模式

与大多数主题一样,API设计存在通用的笼统套路,可以在所有情况下轻松套用,但这些行为往往会使我们误入歧途。毕竟,盲目套用比深入思考API设计并决定如何最好地安排资源以及它们之间的关系要容易得多。

4.3.1 万物皆资源

很多人有时会忍不住给最微小的概念创建资源。通常,这种做法的出现是因为人们认为任何数据类型都必须对应一个适当的资源。例如,想象我们有一个API,可以在图像上存储注释。对于每个注释,我们可能希望存储注释区域(可能以某种边界框住特定面积)以及构成实际注释内容的注释条目列表。

在这一点上,我们有四个独立的概念要考虑:图像、注释、边界框和条目。问题是,其中哪些应该是资源,哪些应该仅仅作为数据类型?一个条件反射的反应是使一切都成为资源,不管是什么。这可能最终看起来像图4.13中所示的资源布局图。

图4.13 将所有概念都作为资源进行布局的示例

现在我们有四个完整的资源,它们具有完整的资源生命周期。换句话说,我们需要为这些资源中的每一个实现五种不同的方法,总共有20个不同的API调用。这带来了一个问题:这是正确的选择吗?毕竟,我们真的需要为每个边界框提供独立的标识符吗?我们真的需要为每个条目创建单独的资源吗?

如果我们更深入地思考这个布局,我们可能会发现两个重要的问题。首先,BoundingBox资源与注释是一对一的关系,因此将其放入单独的资源中几乎没有价值。其次,注释资源的条目列表不太可能变得非常大,因此通过将其表示为资源,我们并没有获得太多的好处。如果我们将这一点考虑在内,我们就会得到一个更简单的资源布局。这意味着我们实际上只需要考虑两个资源(而不是四个),这对于理解和构建API都更容易。图4.14和代码4.2显示了资源布局图的简化版本。

图4.14 将资源简化为2个后的布局示例

代码4.2 通过內联数据简化后的接口表示

// 前两个为实际定义的资源,注意id字段
interface Image {
    id: string;
}
interface Annotation {
    id: string;
    boundingBox: BoundingBox;
    notes: Note[];
}
// 后三个不是资源,他们只是数据类型
interface BoundingBox {
    bottomLeftPoint: Point;
    topRightPoint: Point;
}
interface Point {
    x: number;
    y: number;
}
interface Note {
    content: string;
    createTime: Date;
}

一个好的经验法则是避免两个问题。首先,如果出了与之关联的资源外,您无需与某个资源独立交互,那么它可能仅作为数据类型就可以。在此示例中,不太可能有人需要在图像之外操纵边界框,这意味着这可能边界框很适合作为数据类型。其次,如果您想要与某个事物独立交互(在这种情况下,您可能希望删除单个注释条目或单独创建新条目),如果它可以内联(请参阅第 4.2.2 节),那么这可能是一个不错的选择。在这种情况下,条目资源足够小,并且预计不会在每个注释中增长为一个大集合,这意味着将它们内联而不是表示为独立资源可能是安全的。

4.3.2 深层结构

下一个要避免的常见反模式与层级结构有关。层级关系通常看起来非常强大和有用,我们会尽量在所有可能的地方使用它们。然而,过于深的层级结构可能会对所有相关人员造成困惑,并且难以管理。

例如,让我们想象一下,我们正在构建一个用于管理大学课程目录的系统。我们需要跟踪不同的大学,课程所属的学院或项目(例如,护理学院或工程学院),可用的课程,提供课程的学期,以及各种课程文件(例如,教学大纲)。追踪所有这些信息的一种方法是将所有内容放在单一的层级结构中,如图4.15所示。

图4.15 关于课程目录的深层结构

尽管从技术上讲,这样做是完全可行的,但理解和记住所有不同的父级实在是相当困难。这还意味着我们现在需要知道相当多的信息才能找到一个单一的文档(有关此主题的更多讨论,请参见第6.3.6节),这可能很难存储和回忆。但这里更大的问题是,在这里使用层级结构实际上并不是完成工作所必需的。换句话说,我们可能可以在没有这么多层级结构的情况下创建同样出色的API。

如果我们决定关键部分是大学、课程和文档呢?在这个世界中,学院成为课程的一个字段,学期也是如此。我们可以通过反问学院和学期层级结构是否真的那么关键来发现这一点。

例如,我们计划使用层级结构行为来做什么?我们计划经常删除学院吗?(可能不会。)学期呢?(同样,可能不会。)最有可能的情况是我们希望看到被列出的给定课程的所有学期。但我们可以通过基于课程标识进行过滤来实现这一点,同样,通过给定学院ID列出课程也是如此。

请记住,这并不是说我们应该完全摆脱这些资源。就学期来说,我们不太可能需要一个可用学期的列表以进行查询,但学院可能是值得保留的内容。建议的变更是改变学院和课程之间的关系,从层级结构关系变为引用关系。这种新的(更浅的)层级结构显示在图4.16中。

图4.16 关于课程目录的更浅的层级结构

通过以这种方式(即更浅的方式)安排我们的资源,我们仍然可以执行一些操作,比如授予对给定课程实例的所有文档的访问权限,或者列出给定学期的所有课程,但我们已经将层级结构减少并给到真正受益的部分。

4.3.3 內联一切

随着非关系型数据库的出现,尤其是大规模的键值存储系统,人们往往倾向于去范式化(或内联)所有数据,包括过去会被整齐地组织到单独的表中并通过精巧的SQL查询进行连接的数据。尽管有时将信息折叠到单一资源中而不是使每个信息都成为单独的资源是有意义的(正如我们在第4.3.2节中学到的),但将太多信息折叠到单一资源中可能会像将每个信息分开成单独的资源一样有害。

这其中一个主要原因与数据完整性有关,这也是非关系型数据库中去范式化模式常见的问题。例如,假设我们正在构建一个图书馆API,用于存储不同作者编写的书籍。我们可以选择有两个资源,Book和Author,其中Book资源指向Author资源,如图4.17所示。如果我们选择内联这些信息,我们可能会直接在Book资源上存储作者的姓名(和其他信息),如图4.18所示。

图4.17 书籍和作者分别作为资源

图4.18 将作者內联到书籍资源中

正如我们之前学到的(参见第4.2.2节),像这样内联数据确实很有价值,但它也可能导致一些真实的问题。例如,当您更新书中的一位作者的姓名时,它是否会更新他们所写的所有书籍中的姓名?对于这个问题的答案可能会取决于您的实现,但我们必须提出这个问题就意味着这可能会引起混淆。换句话说,当我们内联数据时,这些数据仍然会在其他资源之间共享(例如,编写了多本书的作者),我们就打开了一个可怕的潘多拉魔盒,我们必须决定API的用户应该如何更新那些共享的数据(例如,作者的姓名)。虽然有很好的方法来解决这些问题,但这实际上不是我们应该首先担心的问题。

4.4 练习

  1. 想象一下,您正在构建一个书签API,其中包含特定在线URL的书签,并可以将这些书签放入不同的文件夹中。您是将其建模为两个单独的资源(Bookmark和Folder),还是一个单一的资源(例如Entity),并通过内联的类型字段指明其是作为文件夹还是书签?
  2. 想出一个例子并绘制一个实体关系图,涉及到一个多对多关系、一个一对多关系、一个一对一关系以及一个可选的一对多关系。确保使用正确的符号来表示连接器。

本章总结

  • 资源布局指的是API中资源之间的排列和关系。
  • 要抵制连接API中的每个资源的冲动,只有在它们对API提供重要功能时才存储资源关系。
  • 有时为一个概念存储一个单独的资源是有意义的。而其他时候,最好将数据内联并将概念保留为数据类型。这个决定取决于您是否需要以原子方式(atomically)与该概念进行交互。
  • 避免过于深层次的层级关系,因为它们可能难以理解和管理。