REST API 用于更新具有空值或空值的信息

REST API for updating informations with empty or null values

我有一个关于如何最好地构建可以修改数据库中记录的 API 的一般性问题。

假设我们有一个包含 10 列的 table,我们可以使用 REST (GET) 查询这 10 列。 JSON 响应将包含所有 10 个字段。这很容易,而且没有问题。

下一步是有人想通过 POST 创建新记录。在这种情况下,此人仅发送 JSON 请求中 10 个字段中的 8 个。然后我们将只填写数据库中的 8 个字段(其余的将为 NULL)。这也没有问题。

但是如果有人想更新记录会怎样呢?我们在这里看到了具有优点和缺点的不同可能性。

  1. 只发送应该更新的内容。 问题:如何显式清空/删除字段?如果在 JSON 中传递了 "NULL",我们将在对象中得到 NULL,但未传递的任何其他字段也为 NULL。因此我们无法区分哪些字段可以删除,哪些字段不能触摸。

  2. 完整对象已发送。 问题:这里的对象之前可以通过 GET 获取,相应地更改并通过 PUT 返回。现在我们得到了所有的信息并且可以直接将信息写回数据库。因为空字段要么之前已经为空,要么被用户清除了。

如果通过 API 的更新扩展对象会发生什么情况。假设我们将数据库再扩展五个字段。 API 的用户进行了 GET,获得了 15 个字段,但只能读取他在页面上知道的 10 个字段(因为他还没有更新他的一侧)。然后他更改了 10 个字段中的一些字段并通过 PUT 将它们发回。然后我们将只更新我们网站上的 10 个字段,并且 5 个新字段将从数据库中清空。

或者您是否必须为每个字段创建一个单独的端点?我们也想过用key/value创建一个map,具体应该改什么。

关于技术:我们使用Wildfly 15 with Resteasy and Jackson

例如:

开头的数据库

+----+----------+---------------+-----+--------+-------+
| ID | Name     | Country       | Age | Weight | Phone |
+----+----------+---------------+-----+--------+-------+
| 1  | Person 1 | Germany       | 22  | 60     | 12345 |
| 2  | Person 2 | United States | 32  | 78     | 56789 |
| 3  | Person 3 | Canada        | 52  | 102    | 99999 |
+----+----------+---------------+-----+--------+-------+

获取.../person/2

{
   "id" : 2,
   "name" : "Person 2",
   "country" : "United States",
   "age" : 22,
   "weight" :62,
   "phone": "56789"
}

现在我想更新他的体重并删除 phone 数字

放.../person/2

{
   "id" : 2,
   "name" : "Person 2",
   "country" : "United States",
   "age" : 22,
   "weight" :78
}

{
   "id" : 2,
   "name" : "Person 2",
   "country" : "United States",
   "age" : 22,
   "weight" :78,
   "phone" : null
}

现在数据库应该如下所示:

+----+----------+---------------+-----+--------+-------+
| ID | Name     | Country       | Age | Weight | Phone |
+----+----------+---------------+-----+--------+-------+
| 1  | Person 1 | Germany       | 22  | 60     | 12345 |
| 2  | Person 2 | United States | 32  | 78     | NULL  |
| 3  | Person 3 | Canada        | 52  | 102    | 99999 |
+----+----------+---------------+-----+--------+-------+

问题是

我们像这样扩展 table (salery)

+----+----------+---------------+-----+--------+--------+-------+
| ID | Name     | Country       | Age | Weight | Salery | Phone |
+----+----------+---------------+-----+--------+--------+-------+
| 1  | Person 1 | Germany       | 22  | 60     | 1929   | 12345 |
| 2  | Person 2 | United States | 32  | 78     | 2831   | NULL  |
| 3  | Person 3 | Canada        | 52  | 102    | 3921   | 99999 |
+----+----------+---------------+-----+--------+--------+-------+

使用 API 的人不知道 JSON 中有一个新的工资字段。而这个人现在又想换一个人的phone号,但是不发工资。这也会清空工资:

{
   "id" : 3,
   "name" : "Person 3",
   "country" : "Cananda",
   "age" : 52,
   "weight" :102,
   "phone" : null
}


+----+----------+---------------+-----+--------+--------+-------+
| ID | Name     | Country       | Age | Weight | Salery | Phone |
+----+----------+---------------+-----+--------+--------+-------+
| 1  | Person 1 | Germany       | 22  | 60     | 1929   | 12345 |
| 2  | Person 2 | United States | 32  | 78     | 2831   | NULL  |
| 3  | Person 3 | Canada        | 52  | 102    | NULL   | NULL  |
+----+----------+---------------+-----+--------+--------+-------+

薪水不应该为空,因为它没有在 JSON 请求中设置

一种常用技术是跟踪实体 POJO 上的更改。

  1. 加载狗 color = 黑色,size = 空,age = 空
  2. size 设置为空(setter 会将此字段标记为已更改)
  3. 运行更新SQL

POJO 将有一个内部状态,知道 size 已更改,因此在 UPDATE 中包含该字段。另一方面,age 从未设置,因此保持不变。 jOOQ 就是这样工作的,我相信还有其他人。

您可以控制空值或空值,如下所示

public class Person{

    @JsonInclude(JsonInclude.Include.NON_NULL) 
    private BigDecimal salary;  // this will make sure salary can't be null or empty//

    private String phone;   //allow phone Number to be empty
    // same logic for other fields
}

i) 当您更新权重并删除 phone 数字时,要求客户端发送需要更新的字段以及记录标识符,即在这种情况下为 id

{
   "id" : 2,
   "weight" :78,
   "phone" : null  
}

ii) 当您将薪水添加为另一列时,这是必填字段,客户应该知道这一点。可能您必须重新设计合同

  1. Only what should be updated is sent. Problem: How can you explicitly empty / delete a field? If a "NULL" is passed in the JSON, we get NULL in the object, but any other field that is not passed is NULL as well. Therefore we cannot distinguish which field can be deleted and which field cannot be touched.

您确定的问题是真实的;我也遇到过这个。我认为为此提供技术解决方案是合理的,而是记录API用法让调用者知道遗漏字段或将其发送为null。当然,假设服务器端的验证是严格的并确保健全。

  1. The complete object is sent. Problem: Here the object could be fetched via a GET before, changed accordingly and returned via PUT. Now we get all information back and could write the information directly back into the database. Because empty fields were either already empty before or were cleared by the user.

这是 "straighter-forward",应该记录在 API 中。

What happens if the objects are extended by an update of the API.

通过文档将责任放在调用者身上,这也是隐式处理的。

Or do you have to create a separate endpoint for each field?

这又是一个设计问题,解决方法因人而异。我宁愿将 API 保持在创纪录的水平,也不愿保持在个人价值的水平。但是,在某些情况下可能需要这样做。例如,状态更新。

Suppose we extend the database by five more fields. The user of the API makes a GET, gets the 15 fields, but can only read the 10 fields he knows on his page (because he hasn't updated his side yet). Then he changes some of the 10 fields and sends them back via PUT. We would then update only the 10 fields on our site and the 5 new fields would be emptied from the database.

那么让我们从一个例子开始 - 在网络上会发生什么,客户通过在浏览器中呈现的 HTML 与您的 API 进行交互。客户端将获取一个表单,该表单将具有每个字段的输入控件。客户更新表单中的字段并提交,然后您将这些更改应用到您的数据库。

当您想要扩展 API 以包含更多字段时,您可以将这些字段添加到表单中。客户不知道这些字段。那么会发生什么?

管理此问题的一种方法是确保在表单中包含新字段的正确默认值;然后,如果客户端忽略新字段,则在提交表单时将返回正确的值。

更一般地说,我们在 HTTP 有效负载中交换的表示是 消息;如果我们想支持老客户,那么我们需要以向后兼容的方式发展消息模式的纪律,并且我们的客户必须在理解消息模式可以扩展额外字段的情况下编写。

The person using the API does not know that there is a new field in JSON for the salary.

这里也有同样的想法 - 新表示包含一个客户端不知道的字段 "salary",因此客户端有责任将该数据转发回给您 未更改,而不是假设它不重要就把它扔在地板上。

15 到 20 年前有大量关于此的现有技术,因为在 XML 中编写消息的人面临着完全相同的问题。他们留下了一些知识。找到它的最简单方法是搜索一些关键阶段;例如 must ignoremust forward.

参见:

事件存储中的事件具有相同类型的问题。 Greg Young's book Versioning in an Event Sourced System 涵盖了很多相同的领域(事件的表示也是消息)。

您可以将 JSON 反序列化为地图。 这样,如果 属性 尚未发送,则 属性 不会出现在地图中。如果它为空,则它在地图内部将为空值。

  ObjectMapper mapper = new ObjectMapper();
  TypeReference<HashMap<String, Object>> typeReference = new TypeReference<>() {};
  HashMap<String, Object> jsonMap = mapper.readValue(json, typeReference);
  jsonMap.entrySet().stream().map(Map.Entry::getKey).forEach(System.out::println);

这不是一个非常方便的解决方案,但它可能适合您。

接受的答案效果很好,但有一个巨大的警告,即它完全没有类型。如果对象的字段发生变化,那么您将没有编译时警告您正在寻找错误的字段。

因此,我认为最好强制所有字段出现在请求正文中。因此,null 意味着用户明确将其设置为 null,而如果用户错过了一个字段,他们将收到一个 400 Bad Request,请求正文详细描述了错误。

这里有一个很好的 post 如何实现这一点:

这是我在 Kotlin 中的示例:

data class PlacementRequestDto(
    val contentId: Int,
    @param:JsonProperty(required = true)
    val tlxPlacementId: Int?,
    val keywords: List<Int>,
    val placementAdFormats: List<Int>
)

请注意,可空字段已标记为必填。这样用户必须明确地将其包含在请求正文中。