用于创建多个关联的网络服务标准方法

webservice standard approaches for creating multiple associations

我正在寻找针对特定 rest web 服务用例的端点设计的建议。

我们有一个基本的 API 可以让特定用户注册特定课程:

PUT /rest/courses/{courseId}/enroll/{studentId}

这很好用,而且很简单。

但是,我们通常有 N 个用户想要注册同一门课程,其中 N 可能是从 1 到 30。我看到两种可能的选择来完成此批量注册:

  1. 调用上述端点N次
  2. 实现一个单独的端点,如下:

    PUT /rest/courses/{courseId}/enroll
    

...并将 N 个学生 ID 放在请求正文中。

选项 1 似乎效率不高。选项 2 有点不对劲(而且似乎不适合 PUT,但也不适合 POST)。这些选项中的任何一个在您看来明显更好(更多"restful")吗?或者有明确的第三个更好的选择吗?

谢谢!

PATCH(提议的 RFC 6902 标准)作为替代方案浮现在脑海中,可能看起来像这样:

{
    "op" : "add",
    "path" : "/rest/courses/{courseId}/students",
    "value" : ["studentId1", "studentId2"]
}

什么是资源表示?

使用 PUT /rest/courses/{courseId}/enroll/{studentId} - 正如您所说,它很简单并且可以正常工作,但是考虑到 REST 是代表性状态转移,使用 REST 时要经常问自己的一个问题是 - 什么是资源状态在该 URI 中表示?

例如如果客户端要调用 GET /rest/courses/{courseId}/enroll/{studentId},将 returned 什么资源表示?仅仅是学生本身,还是他们在该课程中注册的一些细节? (例如包括注册日期)?在其中包含动词 'enroll' 的资源上调用 GET 是否有意义?同一名学生可以多次注册同一门课程吗(即,如果他们在以后的学期重读一门课程?)

另一种可能是:

POST /rest/courses/{courseId}/enrollments
Body:
{
  "studentId" : xyz
}

这将 return 状态代码 201 已创建,并导致在某个位置创建资源,位置 header 设置为该 URI,例如

/rest/enrollments/{enrollmentId}

如果您使用相同的 StudentId 执行另一个 POST,您的业务逻辑将检查是否允许这样做,例如给定注册的学期,如果请求无效,return 400,如果有效,则 201 Created with the 2nd Enrollment.

此约定还允许您定义:

GET /rest/courses/{courseId}/enrollments

哪个可以 return 该课程所有注册的列表,并且

GET /rest/enrollments

哪个可以 return 所有课程的所有注册列表(如果有帮助的话)。

PUT 到 collections

回到你最初的问题,如果你的 URI 应该是一个 'collection' 资源,从技术上讲,一个 PUT 应该替换整个 collection - 因为它在语义上应该是意思是客户端说“这是我想存储在这个资源 URI 中的表示”——所以正如你猜到的那样,它闻起来不对是有原因的。

POST 单个请求中的多个记录

POST却没有这样的约束,实际上是相当灵活的。您可以定义 API 以允许 BODY 包含导致创建多个资源的多个指令,例如

POST /rest/courses/{courseId}/enrollments
BODY
{
  students : [
    {
      studentId : 100
    },
    {
      studentId : 101
    }
  ]
}

但是,允许这样做会带来一些复杂性:

  • 已创建资源的可发现性 - 响应只能包含一个位置 header,因此客户端无法找到所有已创建的注册。您必须在 POST 的响应 body 中包含注册的 URI 列表,而不是使用位置 header
  • 处理部分失败 - 如果添加一名学生失败但另一名学生成功,您如何指示?您必须有一个包含错误消息的 body,这也不是 RESTful,因为理想情况下您应该使用 400 或 500 作为错误条件……或者您必须以原子方式处理批量添加,如果有任何失败,请确保 none 已被处理(例如数据库回滚)
  • 您可能需要定义提交记录数量的限制,或者至少定义 body 的有效负载大小,以免您打开 API 面临 Slow Http Attack depending on how long it takes to process each record. One solution for this might be to make use of 202 Accepted 并异步处理请求 - 但这假设您有支持异步处理的后端基础设施。

一种新的操作方式——一种新的资源?

您的问题提到您认为即使 POST 也不会 RESTful 将多个学生提交到 .../enroll 端点。我认为这部分是因为 enroll(和 enrollment听起来 好像它们只与一个学生有关。另一种方法是定义一个新资源 - bulkEnrollment。例如而

POST /rest/courses/{courseId}/enrollments

表示 'create an enrollment using the POST data',并且只接受一个学生,新资源位于:

POST /rest/courses/{courseId}/bulkEnrollments

表示 'create a bulk enrollment using the POST data' 并接受多个学生 ID。批量注册本身成为您跟踪的实体,然后可以在以下位置检索它的资源表示:

GET /rest/bulkEnrollments/{bulkEnrollmentId}

表示将列出该特定批量操作创建的所有注册,可能包括一些关于批量注册本身的元数据,包括执行此操作的经过身份验证的用户以及他们何时执行此操作。

Hypermedia/HATEOAS

无论您是否公开 bulkEnrollment 资源,也可能值得考虑将超媒体引入您的表现形式 - 例如collection 注册中的每个条目都可以包含超媒体 link 到代表个人注册的资源(例如 /rest/enrollments/{enrollmentId},有时称为 self link) 并且每个注册都可以包含一个超媒体 link 到代表学生的资源(例如 /rest/students/{studentId})。

事实上,超媒体(例如 Hal) is the most RESTful technique you can adopt - far more important that the specific verbs/nouns or structures you use in your endpoints. By only letting the client interact with endpoints that it has discovered by following links previous requests, your API can self-describe the relationships between resources. It also gives you a lot of flexibility as to the actual URI endpoint structure changing over time. But most importantly, it allows you to signal the client as to which operations are actually permitted given the current state of the resource. The concept is known as HATEOAS (the example uses Xml, but Hal can help with the same concepts in Json) and is required for a 'level 3' REST API according to the Richardson Maturity Model for REST APIs.