【问题标题】:REST - put IDs in body or not?REST - 是否将 ID 放入正文?
【发布时间】:2015-01-12 10:38:14
【问题描述】:

假设我想为人们提供一个 RESTful 资源,客户端可以在其中分配 ID。

一个人看起来像这样:{"id": <UUID>, "name": "Jimmy"}

现在,客户端应该如何保存(或“PUT”)它?

  1. PUT /person/UUID {"id": <UUID>, "name": "Jimmy"} - 现在我们有这个令人讨厌的重复,我们必须一直验证:body 中的 ID 是否与路径中的 ID 匹配?
  2. 不对称表示:
    • PUT /person/UUID {"name": "Jimmy"}
    • GET /person/UUID 返回{"id": <UUID>, "name": "Jimmy"}
  3. 正文中没有 ID - ID 仅在位置:
    • PUT /person/UUID {"name": "Jimmy"}
    • GET /person/UUID 返回{"name": "Jimmy"}
  4. POST 似乎不是一个好主意,因为 ID 是由客户端生成的。

常见的模式和解决方法有哪些? IDs only in location 似乎是最教条正确的方法,但它也使实际实施变得更加困难。

【问题讨论】:

    标签: rest


    【解决方案1】:

    拥有不同的读/写模型并没有错:客户端可以编写一个资源表示,然后服务器可以返回另一种表示,其中包含添加/计算的元素(甚至是完全不同的表示 - 任何内容都没有对此进行规范,唯一的要求是 PUT 应该创建或替换资源)。

    所以我会选择(2)中的非对称解决方案,并在编写时避免服务器端的“讨厌的重复检查”:

    PUT /person/UUID {"name": "Jimmy"}
    
    GET /person/UUID returns {"id": <UUID>, "name": "Jimmy"}
    

    【讨论】:

    • 如果你应用打字(静态或动态),你就不能轻松地拥有没有 ID 的模型......所以从 PUT 请求的 URL 中删除 ID 要容易得多。它不会是“宁静的”,但它会是正确的。
    • 在没有 id 的情况下保留额外的 TO 以及带有 id 和实体的 TO 以及额外的转换器,对程序员来说开销太大。
    • 如果我从 BODY 获得 ID 怎么办?例如:PUT /person { "id": 1, "name": "Jimmy" }。那会是个坏习惯吗?
    • 将 ID 放在正文中就可以了。对 ID 使用 GUID 而不是整数 - 否则会有重复 ID 的风险。
    • 这是错误的。看我的回答。 PUT 必须包含整个资源。如果您想排除 id 并仅更新记录的一部分,请使用 PATCH。
    【解决方案2】:

    如果是公开的API,你在回复的时候要保守,但要大方地接受。

    我的意思是,你应该同时支持 1 和 2。我同意 3 没有意义。

    同时支持 1 和 2 的方法是,如果请求体中没有提供 id,则从 url 中获取 id,如果它在请求体中,则验证它是否与 url 中的 id 匹配。如果两者不匹配,则返回 400 Bad Request 响应。

    返回人员资源时要保守,始终在 json 中包含 id,即使它在 put 中是可选的。

    【讨论】:

    • 这应该是公认的解决方案。 API 应该始终是用户友好的。它在正文中应该是可选的。我不应该从 POST 接收 ID,然后必须在 PUT 中将其设为未定义。此外,400 响应点是正确的。
    • 大约 400 个代码参见softwareengineering.stackexchange.com/questions/329229/… 讨论。简而言之,与 422 相比,400 代码并非不合适,只是不太具体。
    • 我有点不同意,但没有反对意见。我认为 API 应该是严格的。如果我想用 API 做某事,那么它应该是一种方式(并且根据文档),而不是可选的,例如URI 与正文。如果我们将其放宽,则允许多个选项对 API 进行更多的代码和维护 - 如果主体为空,则检查 URI,反之亦然。此外,它破坏了一致性,不仅在客户端调用和服务器在此 API 上接收的方式上,而且在整个互联网上也是如此。如果我们都同意这些事情的一种方式并且每个人都坚持下去,那就更好了。较少主观的建议/指南
    【解决方案3】:

    此问题的一个解决方案涉及“超文本作为应用程序状态的引擎”或“HATEOAS”这一有点令人困惑的概念。这意味着 REST 响应包含要作为超链接执行的可用资源或操作。使用这种方法(它是 REST 原始概念的一部分),资源的唯一标识符/ID 本身就是超链接。所以,例如,你可以有类似的东西:

    GET /person/<UUID> {"person": {"location": "/person/<UUID>", "data": { "name": "Jimmy"}}}
    

    然后,如果您想更新该资源,您可以执行(伪代码):

    updatedPerson = person.data
    updatedPerson.name = "Timmy"
    PUT(URI: response.resource, data: updatedPerson)
    

    这样做的一个优点是客户端不必了解服务器的用户 ID 的内部表示。 ID 可以改变,甚至 URL 本身也可以改变,只要客户端有办法发现它们。例如,当获取人员集合时,您可以返回如下响应:

    GET /people
    { "people": [
        "/person/1",
        "/person/2"
      ]
    }
    

    (当然,您也可以为每个人返回完整的人员对象,具体取决于应用程序的需要)。

    使用这种方法,您可以更多地从资源和位置方面考虑您的对象,而不是从 ID 方面考虑。因此,唯一标识符的内部表示与您的客户端逻辑分离。这就是 REST 背后的最初动力:通过使用 HTTP 的特性,创建比以前存在的 RPC 系统更松耦合的客户端-服务器架构。有关 HATEOAS 的更多信息,请查看Wikipedia article 以及此short article。

    【讨论】:

      【解决方案4】:

      仅供参考,这里的答案是错误的。

      见:

      https://restfulapi.net/rest-api-design-tutorial-with-example/

      https://restfulapi.net/rest-put-vs-post/

      https://restfulapi.net/http-methods/#patch

      PUT

      主要使用 PUT API 来更新现有资源(如果 资源不存在,那么 API 可能会决定创建一个新资源 或不)。如果 PUT API 创建了新资源,则源 服务器必须通过 HTTP 响应代码 201 通知用户代理 (已创建)响应,如果修改了现有资源,则 应该发送 200 (OK) 或 204 (No Content) 响应代码来指示 成功完成请求。

      如果请求通过缓存并且 Request-URI 标识 一个或多个当前缓存的实体,这些条目应该被处理 作为陈旧的。对此方法的响应不可缓存。

      当你想修改已经是一个单一资源时,使用 PUT 资源收集的一部分。 PUT 替换它的资源 整体。如果请求更新了部分资源,请使用 PATCH。

      补丁

      HTTP PATCH 请求是对资源进行部分更新。如果你 请参阅 PUT 请求还修改资源实体,以便更清楚 - PATCH 方法是部分更新现有的正确选择 resource 和 PUT 仅应在您要替换资源时使用 完整的。

      所以你应该这样使用它:

      POST    /device-management/devices      : Create a new device
      PUT     /device-management/devices/{id} : Update the device information identified by "id"
      PATCH   /device-management/devices/{id} : Partial-update the device information identified by "id"
      

      RESTful 实践表明,您在 /{id} 放置什么内容并不重要——记录的内容应更新为有效负载提供的内容——但 GET /{id} 仍应链接到相同的资源。

      换句话说,PUT /3 可能会将有效负载 id 更新为 4,但 GET /3 仍应链接到相同的有效负载(并返回 id 设置为 4 的有效负载)。

      如果您决定您的 API 需要在 URI 和有效负载中使用相同的标识符,则您的工作是确保它匹配,但如果您排除有效负载中应该存在的 id,请务必使用 PATCH 而不是 PUT完整的。这就是接受的答案出错的地方。 PUT 必须替换整个资源,而补丁可能是部分的。

      【讨论】:

      • 在您的段落中:“换句话说,PUT /3 可能会将有效负载 id 更新为 4,但 GET /3 仍应链接到相同的有效负载(并返回 id 设置为 4)。” - 您是说最后一个 id 中的 3 而不是 4 吗?
      • 回复:“如果您排除 id,请使用 PATCH 而不是 PUT”。 这是不明智的。 2014 年的 RFC 7231(废弃 1999 年的 RFC 2616 的 RFC 之一)提到 here 服务器可以有 transformation applied to the body。甚至还有一种客户端机制allows a user agent to know when the representation body it has in memory remains current,即:服务器MUST NOT send ... an ETag or Last-Modified ... unless the request's representation was saved without any transformation。
      • 另请参阅this SO question+answer from other users,其中包含使用 PUT 的理由。它很好地总结了“重要的是要了解客户端请求的意图是什么。客户端打算用传递的值完全替换资源的内容。”
      • OP 询问了 RESTful 实践。 PATCH 允许您更新存在的资源部分,其中 PUT 替换资源的所有部分或插入不存在的资源。如果您排除部分资源,我不会使用 PUT。这就是 PATCH 的用途。
      • @mlst 这不是一个错误。如果您在有效负载中使用 id 4 PUT /3,您的服务器应该使用 PUT 的完整内容更新记录。这是因为 /3 不必与有效负载中的 id 具有相同的含义。如果 /3 应该是相同的并且不改变 bc 你的应用程序这么说,你应该拒绝 PUT。有些人可以通过 UUID 对 res 进行标识,例如 (PUT /:UUID),并且在有效负载中具有不同的 int id。如果是这种情况,在你 PUT /:UUID 一个新的有效载荷 id 之后,调用 GET /:UUID 会返回你 PUT 的内容。 REST 没有明确地将 res 绑定到一个 db id,但它可以。
      【解决方案5】:

      在插入中,您不需要在 URL 中添加 id。这样,如果您在 PUT 中发送 ID,您可能会将其解释为 UPDATE 以更改主键。

      1. 插入:

        PUT /persons/ 
          {"id": 1, "name": "Jimmy"}
        HTTP/1.1 201 Created     
          {"id": 1, "name": "Jimmy", "other_field"="filled_by_server"}
        
        GET /persons/1
        
        HTTP/1.1 200 OK
          {"id": 1, "name": "Jimmy", "other_field"="filled_by_server"}  
        
      2. 更新

        PUT /persons/1 
             {"id": "2", "name": "Jimmy Jr"} - 
        HTTP/1.1 200 OK
             {"id": "2", "name": "Jimmy Jr", "other_field"="filled_by_server"}
        
        GET /persons/2 
        
        HTTP/1.1 200 OK
             {"id": "2", "name": "Jimmy Jr", "other_field"="updated_by_server"}
        

      JSON API 使用此标准并解决了返回插入或更新的对象以及指向新对象的链接的一些问题。某些更新或插入可能包含一些会更改其他字段的业务逻辑

      您还将看到您可以避免插入和更新后的获取。

      【讨论】:

        【解决方案6】:

        虽然可以为不同的操作使用不同的表示形式,但对 PUT 的一般建议是包含整个有效负载。这意味着id 也应该在那里。否则,您应该使用 PATCH。

        话虽如此,我认为 PUT 应该主要用于更新,id 也应该始终在 URL 中传递。因此,使用 PUT 更新资源标识符是一个坏主意。 当 URL 中的 id 可能与正文中的 id 不同时,它会让我们处于不利的境地。

        那么,我们如何解决这样的冲突呢?我们基本上有两种选择:

        • 抛出 4XX 异常
        • 添加 Warning(X-API-Warn 等) 标头。

        我已经可以回答这个问题了,因为这个话题通常是一个见仁见智的问题。

        【讨论】:

          【解决方案7】:

          这已经被问过 - 讨论值得一看:

          Should a RESTful GET response return a resource's ID?

          这是很容易陷入围绕what is and is not "RESTful" 辩论的问题之一。

          对于它的价值,我尝试从一致的资源的角度来思考,而不是在方法之间改变它们的设计。但是,恕我直言,从可用性的角度来看,最重要的是您在整个 API 中保持一致!

          【讨论】:

            【解决方案8】:

            使用不同的方法并没有什么不好。但我认为最好的方法是 with 2nd 的解决方案。

             PUT /person/UUID {"name": "Jimmy"}
            
             GET /person/UUID returns {"id": <UUID>, "name": "Jimmy"}
            

            大多是这样使用的实体框架也使用这种技术当实体被添加到dbContext中时,没有生成ID的类就是实体框架中通过引用生成的ID。

            【讨论】:

              【解决方案9】:

              您可能需要查看 PATCH/PUT 请求类型。

              PATCH 请求用于部分更新资源,而在 PUT 请求中,您必须将整个资源发送到服务器上被覆盖的位置。

              就 url 中的 ID 而言,我认为您应该始终拥有它,因为这是识别资源的标准做法。甚至 Stripe API 也是如此。

              您可以使用 PATCH 请求更新服务器上的资源,并使用 ID 来识别它,但不要更新实际 ID。

              【讨论】:

                【解决方案10】:

                我从JSON-LD/ 语义Web 的角度来看这个问题,因为这是实现真正的REST 一致性的好方法,正如我在these slides 中概述的那样。从这个角度来看,选择 (1.) 是毫无疑问的,因为 Web 资源的 ID (IRI) 应该始终等于我可以用来查找/取消引用资源的 URL。 我认为验证并不难实现,也不是计算密集型;所以我不认为这是选择选项 (2.) 的正当理由。 我认为选项 (3.) 并不是一个真正的选项,因为 POST(新建)与 PUT(更新/替换)具有不同的语义。

                【讨论】:

                  猜你喜欢
                  • 2010-12-22
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2014-03-07
                  • 1970-01-01
                  • 2012-03-20
                  • 1970-01-01
                  相关资源
                  最近更新 更多