是否应在API中返回自然密钥或代理密钥?

时间:2015-02-04 07:37:48

标签: rest consistency optimistic-locking

我第一次想到它......

到目前为止,我总是在API中使用自然键。例如,一个允许处理实体的REST API,URL就像/entities/{id},其中id是用户已知的自然键(ID被传递给创建实体的POST请求)。创建实体后,用户可以使用多个命令(GET,DELETE,PUT ...)来操作实体。该实体还具有由数据库生成的代理键。

现在,请考虑以下顺序:

  1. 用户创建身份为1的实体(POST /entities,正文包含身份1)
  2. 其他用户删除实体(DELETE /entities/1
  3. 同一个其他用户再次创建实体(POST /entities,其身体包含ID为1)
  4. 第一个用户决定修改实体(PUT /entities/1 with body)
  5. 在执行第4步之前,数据库中仍然存在id为1的实体,但它与步骤1中创建的实体不同。问题是第4步根据自然键识别要修改的实体对于已删除的实体和新实体是相同的(代理键不同)。因此,步骤4将成功,用户永远不会知道它正在处理新实体。

    我通常也会在我的应用程序中使用乐观锁定,但我认为这对它没有帮助。在步骤1之后,实体的版本字段为0.在步骤3之后,新实体的版本字段也为0.因此,版本检查不会有帮助。是否正确使用时间戳字段进行乐观锁定?

    "好"解决方案将代理键返回给用户?这样,用户总是向服务器提供代理键,可以使用它来确保它在同一个实体上运行而不是在新实体上运行吗?

    您推荐哪种方法?

    谢谢, 迈克尔

4 个答案:

答案 0 :(得分:3)

这取决于您希望用户如何使用您的API。

REST API应该尝试被发现。因此,如果在API中公开自然键有好处,因为它允许用户直接修改URI并进入新状态,那么就这样做。

一个很好的例子是类别或标签。我们可以使用以下URI;

GET /some-resource?tag=1    // returns all resources tagged with 'blue'
GET /some-resource?tag=2    // returns all resources tagged with 'red'

GET /some-resource?tag=blue    // returns all resources tagged with 'blue'
GET /some-resource?tag=red    // returns all resources tagged with 'red'

第二组中的用户显然更有价值,因为他们可以看到标签是真正的单词。然后,这允许他们在那里键入任何单词以查看返回的内容,而第一组不允许这样:它限制可发现性

另一个例子是订单

GET /orders/1   // returns order 1

GET /orders/some-verbose-name-that-adds-no-meaning    // returns order 1

在这种情况下,向订单添加一些详细名称以使其可被发现几乎没有价值。用户更可能希望首先查看所有订单(或子集)并按日期或价格等过滤,然后选择要查看的订单

GET /orders?orderBy={date}&order=asc

其他

在我们讨论聊天之后,您的问题似乎与版本控制以及如何管理资源锁定有关。

如果允许多个用户修改资源,则需要发送包含每个请求和响应的版本号。进行任何更改时,版本号会递增。如果请求在尝试修改资源时发送旧版本号,则抛出错误。

如果您允许重复使用相同的URI,则可能会发生冲突,因为版本号始终从0开始。在这种情况下,您还需要通过GUID(代理键)和版本号。或者不使用自然URI(请参阅上面的原始答案以决定何时执行此操作)。

还有另一种选择是禁止重用URI。这实际上取决于用例和业务需求。重用URI可能没什么问题,因为概念上它意味着同样的事情。例如,如果您的计算机上有一个文件夹。删除文件夹并重新创建它与清空文件夹相同。从概念上讲,文件夹是相同的东西'但具有不同的属性。

用户帐户可能是重用URI不是一个好主意的区域。如果删除帐户/ accounts / u1,则该URI应标记为已删除,其他用户无法创建用户名为u1的帐户。从概念上讲,使用相同URI的新用户与前一个用户使用它时的用户不同。

答案 1 :(得分:0)

您提到了使用时间戳作为乐观锁定的可能性。

根据您遵循RESTful原则的严格程度,POST返回的实体将包含"编辑自我"链接;这是可以执行DELETE或UPDATE的URI。

以上述步骤为例:

第1步

用户A执行实体1的POST。返回的实体对象将包含" self"指示更新应该发生的链接,例如:

/entities/1/timestamp/312547124138

第2步

用户B获取现有的实体1,上面的" self"链接,并对该时间戳版本化的URI执行DELETE。

第3步

用户B执行新实体1的POST,该实体返回具有不同" self"的对象。链接,例如:

/entities/1/timestamp/312547999999

第4步

用户A,他们在步骤1中获得的原始实体,尝试对自己进行PUT"链接他们的对象,这是:

/entities/1/timestamp/312547124138

...您的服务会认识到尽管实体1确实存在;用户A正在对一个已经过时的版本尝试PUT。

然后,服务可以执行适当的操作。根据算法的复杂程度,您可以合并更改或拒绝PUT。

我无法记住您应该返回的适当的HTTP状态代码,在PUT到过时的版本之后...这不是我在Rest框架中实现的东西虽然我计划在将来启用它,但仍在努力。可能是你返回410(" Gone")。

第5步

我知道你没有第5步,但是......!用户A在发现他们的PUT失败后,可能会重新检索实体1.这可能是他们(陈旧)版本的GET,即GET:

/entities/1/timestamp/312547124138

...并且您的服务将从该对象的通用URI返回重定向到GET,例如:

/entities/1

...或特定的最新版本,即:

/entities/1/timestamp/312547999999

然后,他们可以根据任何应用程序级别的合并逻辑进行步骤4中的更改。

希望有所帮助。

答案 2 :(得分:0)

您的问题可以使用ETag进行版本控制(记录只能在提供当前ETag时进行修改)或软删除(因此删除的记录仍然存在,但有一个由PUT重置的已删除的bool)。

听起来您也可能从批处理终点和使用交易中受益。

答案 3 :(得分:0)

看到人们试图重新发现已知问题的解决方案很有趣。此问题并非特定于REST API,而是适用于所有索引存储。我见过的唯一实现的解决方案是不要重用代理键

如果要在客户端生成代理密钥,请使用UUID或拆分序列,但最好在服务器端使用它。

此外,如果数据中存在简单的自然键,则应该从不使用代理键来取消引用数据。实际上,即使自然键是复合实体,您也应该非常仔细地考虑是否在API中公开代理键。