我正在努力使用REST API设计概念。我有这些课程:
user:
- first_name
- last_name
metadata_fields:
- field_name
user_metadata:
- user_id
- field_id
- value
- unique index on [user_id, field_id]
好的,因此用户拥有许多元数据,元数据类型在metadata_fields中定义。典型的HABTM,在连接表中有额外的数据。
如果我要通过Rails表单更新user_metadata,数据将如下所示:
user_metadata: {
id: 1,
user_id: 2,
field_id: 3,
value: 'foo'
}
如果我发布给用户#update controller,数据将如下所示:
user: {
user_metadata: {
id: 1,
field_id: 3,
value: 'foo'
}
}
这种方法的问题在于我们忽略了user_id / field_id关系的唯一性。如果我在任一更新中更改field_id,我不仅仅是更改数据,而是更改数据的含义。这在Rails中可以正常工作,因为它有点像围墙花园,但是当你打开一个API端点时会崩溃。
如果我允许的话:
PATCH /api/user_metadata
然后我将自己打开给修改user_id或field_id或两者的人。与此类似:
PATCH /api/user/:user_id/metadata
现在设置了user_id但是field_id仍然可以更改。所以解决这个问题的唯一方法是将更新限制为单个字段:
PATCH /api/user/:user_id/metadata/:field_id
或批量更新:
PATCH /api/user/:user_id/metadata
但是通过该调用,我们必须修改数据结构,以便user_id / field_id关系的唯一性保持不变:
user_metadata: {
field_id1: 'value1',
field_id2: 'value2',
...
}
我喜欢听到这里的想法。我已经搜索过谷歌并且一无所获。有什么建议吗?
答案 0 :(得分:3)
由于元数据属于某个用户/api/user/{userId}/metadata/{metadataId}
,因此可能是用户的单个元数据资源的干净URI。资源的URI已经是您要查找的唯一键。不能有2个具有相同URI的资源!此外,URI已包含用户和字段ID。
像GET /api/user/1 HTTP/1.1
这样的请求可能会返回类似HAL的表示,如下所示:
{
"user" : {
"id": "1",
"firstName": "Max",
"lastName": "Sample",
...
"_links": {
"self" : {
"href": "/api/user/1"
}
},
"_embedded": {
"metadata" : {
"fields" : [{
"id": "1",
"type": "string",
"value": "foo",
"_links": {
"self": {
"href": "/api/user/1/metadata/1"
}
}
}, {
"id": "2",
"type": "string",
"value": "bar",
"_links": {
"self": {
"href": "/api/user/1/metadata/2"
}
}
}],
"_links": {
"self": {
"href": "/api/user/1/metadata"
}
}
}
}
}
}
当然,您可以发送PUT
或PATCH
请求来修改现有元数据字段。但是,资源的URI仍然相同(除非您在PATCH
请求中移动或删除资源)。
您还可以忽略导致PUT
请求的某些字段,从而阻止修改某些字段,例如id
或_link
。我假设这也应该对PATCH
请求有效,但是因此必须重新阅读规范。
因此,我建议忽略请求中包含的所有id
或_link
字段,并更新其余字段。但是,如果有人尝试更新ID字段,您还可以选择返回403 Forbidden
或409 Conflict
响应。
<强>更新强>
如果要在单个请求中更新多个字段,则有两个选项:
PUT
并将当前字段集替换为新版本PATCH
并向服务器发送必要步骤,将当前字段集转换为新字段集 示例PUT
:
PUT /api/user/1/metadata HTTP/1.1
{
"metadata": {
"fields": [{
"type": "string",
"value": "newFoo"
}, {
"type": "string",
"value": "newBar"
}]
}
}
此请求将首先删除元数据所属的用户的每个存储的元数据字段,然后为请求中的每个包含字段创建新的资源。虽然这仍然保证了唯一的URI,但是这种方法存在一些缺点:
/user/1/metadata/2
,通过自动递增调度ID,然而更新引入了新的第二个项目,因此将前者2移动到位置3,client1现在已经但是,实际数据为/user/1/metadata/2
时,引用/user/1/metadata/3
。为防止这种情况,可以使用唯一的UUID代替自动增量ID。如果客户端1稍后尝试检索或更新以前的资源2,则可以通知他该资源不再可用,甚至可以创建重定向到新位置。 示例PATCH
:
PATCH
请求包含将资源状态转换为新状态的必要步骤。请求本身可以同时影响多个资源,甚至可以根据需要创建或删除其他资源。
以下示例采用json-patch+json
格式:
PATCH /api/user/1/metadata HTTP/1.1
[
{
"op": "add",
"path": "/0/value",
"value": "newFoo"
},
{
"op": "add",
"path": "/2",
"value": { "type": "string", "value": "totally new entry" }
},
{
"op": "remove",
"path": "/1"
},
]
路径被定义为被调用资源的JSON Pointer。
JSON-Patch类型的add
操作定义为:
- 如果目标位置指定了数组索引,则会在指定索引处的数组中插入新值。
- 如果目标位置指定了尚不存在的对象成员,则会向该对象添加新成员。
- 如果目标位置指定了确实存在的对象成员,则替换该成员的值。
但是对于删除案例,规范说明:
如果从数组中删除元素,则指定索引上方的任何元素都会向左移动一个位置。
因此,新添加的条目将最终位于数组中的第2位。如果没有自动增量值用于ID,这应该不是一个大问题。
Besindes add
和remove
规范还包含replace
,move
,copy
和test
的定义。
PATCH
应该是事务性的 - 要么所有操作都成功,要么都没有。规范说明:
如果JSON补丁文档违反了规范性要求,或者操作不成功,则JSON补丁文档的评估应该终止并且整个补丁文档的应用不会被视为成功。
我将此行解释为,如果它尝试更新不应更新的字段,则应该为整个PATCH
请求返回错误,因此不会更改任何资源。
回归PATCH
方法显然是交易要求以及JSON指针符号,它可能不那么受欢迎(至少我没有经常使用它并且不得不重新查找它)。与PUT
相同,PATCH
允许在现有资源之间添加新资源,并将更多资源转移到右侧,如果您依赖自动增量值,可能会导致问题。
因此,我强烈建议使用随机生成的UUID作为标识符而不是自动递增值。