更新/创建分层资源REST

时间:2012-11-05 15:28:21

标签: rest

我正在研究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"}我收回了新的userid = 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"]}
  4. 我的担忧:

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

4 个答案:

答案 0 :(得分:16)

您的问题得到妥善解决,问题清单正确无误。如果我建议,你的方法看起来非常像你正在使用关系数据库方法并且正在执行INSERT,从用于下一个INSERT的序列中检索PK,依此类推。

让服务器保持参照完整性

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

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

发布整个文档

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

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

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

  1. 交易可以是原子的
  2. 客户端和服务器之间只有一个网络交换
  3. 服务器执行繁重的工作并且可以验证整个事务(例如,如果配置文件无效,则不会创建用户)
  4. 无需第3步。
  5. 请注意,除了上面详述的之外,您使用此方法 - 您仍然希望能够直接访问用户的个人资料。

    GET - 客户可以指定字段

    与来自知名公司的API进行比较很有意思。以LinkedIn为例。在developer API中,用户的默认GET只返回用户的姓名,标题和URI。

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

    用于更新文档属性的PATCH

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

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

    将取消设置u1,将u2设置为new-foo并将配置文件的p1更新为new-baz。请注意,如果缺少某个字段(p2),则不会修改该字段。如this answer中所述,PATCH优于较旧的PUT。

    如果您只需要更新个人资料,请将新的个人资料记录直接修改为http://api.example.com/users/42/profile

    DELETE应该级联

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

答案 1 :(得分:2)

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

为方便访问,我们可以说User.profile包含href linkrel == 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

通过取消引用文档上的hrefs而不是构建具有id的URL,您可以从响应中获取,您的客户端不必在API更改时进行更改。喜欢的时候    - 个人资料已移至http://profiles.cdn.example.com/    - 个人资料获得p3值

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

答案 2 :(得分: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

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

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

答案 3 :(得分: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创建用户时,它应该只创建一个用户而不是其他任何东西,但实际上是配置文件是强制性的,它不像看起来那么丑陋。)

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

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

请查看此document以获取REST API最佳做法

也许您还可以查看试图处理对象之间关系的HAL

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