21

我正在研究一个REST API并且我正在尝试了解如何处理分层资源。

背景

让我们从一个简单的例子开始。在我的 API 中,我有用户用户个人资料评论

  • 用户必须关联一个用户配置文件(一个用户配置文件只对应一个用户)
  • 用户可能有关联的评论(评论仅对应一个用户)

用户的资源表示应该是:

User: {
   "u1": "u1value", // User's attributes
   "u2": "u2value",
   ...
   "links": [{
       "rel": "profile",
       "href": "http://..." // URI of the profile resource
   }, {
       "rel": "review",
       "href": "http://..." // URI of the review resource
   }]
}

用户配置文件资源表示应该是:

UserProfile: {
   "p1": "p1value", // Profile attributes
   "p2": "p2value",
   ...
   "links": [{
       "rel": "owner",
       "href": "http://..." // URI of the user resource
   }]
}

评审资源表示应该是:

Review: {
   "r1": "r1value", // Review attributes
   "r2": "r2value",
   ...
   "links": [{
       "rel": "owner",
       "href": "http://..." // URI of the user resource
   }]
}

资源 URI 可以是:

  • http://api.example.com/users/{userid}: 访问用户资源
  • http://api.example.com/users/{userid}/profile:访问用户的个人资料资源
  • http://api.example.com/users/{userid}/review: 访问用户的评论资源

资源创建:创建用户的正确方法是什么?

现在我想创建一个新用户:

  1. POST http://api.example.com/users {"u1": "bar", "u2": "foo"}我取回了新的用户 ID = 42
  2. POST http://api.example.com/users/42/profile {"p1": "baz", "p2": "asd"}
  3. PUT http://api.example.com/users {"u1": "bar", "u2": "foo", links: [{"rel": "profile", "href": "http://api.example.com/users/42/profile"]}

我的担忧:

  • 如果在 1 和 2 或 2 和 3 之间出现中断怎么办?
  • 在 3) 中,服务器是否应该自动更新http://api.example.com/users/42/profile中的链接以指向正确的所有者?
  • 更新链接字段是创建关系的正确方式吗?还是应该跳过第 3 步)让系统根据 URI 约定猜测关系?(我在几本书上读到 URI 应该被认为是不透明的。)
4

4 回答 4

17

您的担忧是正确的,并且您的问题清单是正确的。如果我可以建议,您的方法看起来非常像您正在使用关系数据库方法并且正在执行 INSERT,从用于下一个 INSERT 的序列中检索 PK,等等。

让服务器保持参照完整性

作为观察,即使遵循您的原始方案,也完全省略第 3 步。检索用户文档时可见的 URIlinks应由服务器根据配置文件记录的存在生成。

例如,如果使用关系后端,则从 USERS 中选择以获取用户记录。接下来,您从 PROFILES 中选择。如果有记录,则修改返回数据结构以包含引用。

发布整个文件

解决您提出的其他问题的常用方法是允许将整个文档发布到用户 URL(如 NoSQL 数据库,如MongoDB)。这里的文档是用户个人资料:

{ 
   "u1": "bar",
   "u2": "foo",
   "profile": {  
                 "p1": "baz",
                 "p2": "asd"
              }
}

在这种方法中,服务器上的端点接收嵌套结构(文档)并执行 INSERT 到 USERS,检索 PK,然后使用此 PK 执行 INSERT 到 PROFILES。在服务器端这样做可以解决几个问题:

  1. 事务可以是原子的
  2. 客户端和服务器之间只有一个网络交换
  3. 服务器完成繁重的工作并可以验证整个交易(例如,如果配置文件无效,则不会创建用户)
  4. 不需要第 3 步。

请注意,这种方法是对上面详述的 API的补充——您仍然希望能够直接访问用户的个人资料。

GET - 客户端可以指定字段

与知名公司的 API 进行比较很有趣。以领英为例。在他们的开发者 API中,用户的默认 GET 只返回用户名、标题和 URI。

但是,如果请求指定了其他字段,您可以获得嵌套数据,例如http://developer.linkedin.com/documents/understanding-field-selectors中的第二个示例返回用户名和职位的公司名称列表他们举行了。您可以为配置文件和评论实施类似的方案。

用于更新文档属性的补丁

通过插入和查询,可能值得考虑如何更新(PATCH)数据。覆盖一个字段是显而易见的,所以你可以,例如 PATCH 到http://api.example.com/users/42如下:

{ 
   "u1": null,
   "u2": "new-foo",
   "profile": {  "p1": "new-baz"}
}

它将取消设置u1,设置u2new-foo并将配置文件更新p1new-baz. 请注意,如果缺少字段 ( p2),则不会修改该字段。PATCH 比旧的 PUT 更可取,如本答案中所述。

如果您只需要更新配置文件,请将新的配置文件记录直接 PATCH 到http://api.example.com/users/42/profile

DELETE 应该级联

最后,删除可以使用指向要删除的资源的 DELETE 方法来完成 - 无论是用户、个人资料还是评论。实施级联删除,以便删除用户删除他/她的个人资料和评论。

于 2013-04-02T02:02:33.117 回答
2

您应该坚持HATEOAS,并取消引用您在回复中获得的 URL:

为了便于访问,假设User.profile包含with 。hreflinkrel == profile

创建用户

使用您描述的 POST ......但它不应该返回一个 ID,而是一个用户,并带有它的链接。

User: {
   "u1": "bar", // User's attributes
   "u2": "foo",
   ...
   "profile": "http://api.example.com/users/42/profile",
   "links": [{
       "rel": "profile",
       "href": "http://api.example.com/users/42/profile"
   },
   ...
   ]
}

此时,User.profile 中的配置文件资源(可能是http://api.example.com/users/42/profile,或您将来迁移到的任何位置)是默认配置文件应该是的任何内容,例如空文档或仅填充所有者链接。

更新个人资料

profile = GET User.profile
profile.p1 = "baz"
profile.p2 = "asd"
PUT profile to the same url you just dereferenced

通过取消引用文档上的 href,而不是使用您从响应中获得的 id 构建 url,您的客户端将不必在 API 更改时进行更改。就像当 - 配置文件移动到http://profiles.cdn.example.com/ - 配置文件获得 p3 值

“旧” API 客户端将继续工作,而无需更改任何代码。

于 2013-04-08T10:49:32.447 回答
1

实际上,从成功的步骤 (1) 开始,您应该获得 HTTP 代码 201 Created 和新创建资源的地址 (URL),而不仅仅是 ID 号。如果步骤 (2) 失败,您的 REST API 应指示问题出在客户端,例如格式错误的文档(问题代码 4xx)或服务器(5xx)。例如,如果同时删除了资源 42,则应返回代码 404 Not Found。

这就是无状态 REST API 的问题——它们不能支持由多个请求组成的事务。为此,您必须在服务器上维护一个会话(状态)。

顺便说一句,您示例中步骤 (3) 中的 URL 表明您正在替换所有用户,并且可能应该阅读http://api.example.com/users/42

您可以选择一次提交完整的用户+配置文件以在一个原子事务中拆分为两个数据库记录,或者允许部分用户数据的持久性,即没有配置文件的用户。

选择取决于上下文。例如,用户没有配置文件可能完全没问题(因此它可以由用户提供)。相反,拥有不属于任何用户的个人资料记录可能是不可接受的。关于强制执行此逻辑的讨论超出了您的问题范围,并且会因您选择的持久存储(数据库)的类型而异。关系数据库使用外键强制执行此操作。

于 2013-04-04T06:56:56.927 回答
0

我相信你的电话应该是这样的

1)创建用户

POST http://api.example.com/users + params in payload

如果它返回 HTTP 201 + 用户 ID,那么您可以继续创建配置文件。否则,您可以按照自己的方式处理异常。在开始创建配置文件之前,您应该等待第一个呼叫返回。

2) 创建与用户 42 关联的配置文件(如果用户创建正常)

POST http://api.example.com/users/42/profile + params in payload

返回 HTTP 201 + 配置文件 ID

您的后端将负责更新您的用户对象和配置文件对象(以及您的数据库),以便将用户 42 链接到新配置文件。如果后端无法链接对象,您可以发回 500 错误来解释发生了什么。

所以在我看来,我会跳过第 3 步。

现在我明白你的意思是用户必须有个人资料

我看到了 2 个解决方案

1)您可以在创建用户时创建一个空的关联配置文件。然后通过查询用户,您可以获得配置文件 ID 并使用 PUT 修改它(我真的不喜欢这个解决方案,因为当您要求您的 api 创建用户时,它应该只创建一个用户,而不是其他任何东西,但实际上是profile 是强制性的,它不像看起来那么难看)。

2)您可以在您的用户中拥有一个属性,说明用户是否正确(意味着他有相关的个人资料)。在创建过程结束时,如果您的用户 42 不正确,您可以将其删除或重试配置文件创建...然后您可以仅使用 /users?isCorrect=true 之类的内容查询正确的用户

3)让客户将用户视为没有个人资料的事实 - >显示一个弹出窗口以要求创建个人资料......

查看此文档以了解 REST API 最佳实践

也许你也可以看看HAL,它试图处理对象之间的关系。

最后但并非最不重要的一点是,您可以关注api-craft google group,您可能会在那里找到与您的问题相关的有趣主题。

于 2013-04-05T12:16:55.147 回答