如何区分Spring Rest Controller中部分更新的null值和未提供值

我试图在Spring Rest Controller中使用PUT请求方法部分更新实体时,区分空值和未提供的值。

以下面的实体为例:

@Entity private class Person { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; /* let's assume the following attributes may be null */ private String firstName; private String lastName; /* getters and setters ... */ } 

我的人员库(Spring Data):

 @Repository public interface PersonRepository extends CrudRepository { } 

我使用的DTO:

 private class PersonDTO { private String firstName; private String lastName; /* getters and setters ... */ } 

我的Spring RestController:

 @RestController @RequestMapping("/api/people") public class PersonController { @Autowired private PersonRepository people; @Transactional @RequestMapping(path = "/{personId}", method = RequestMethod.PUT) public ResponseEntity update( @PathVariable String personId, @RequestBody PersonDTO dto) { // get the entity by ID Person p = people.findOne(personId); // we assume it exists // update ONLY entity attributes that have been defined if(/* dto.getFirstName is defined */) p.setFirstName = dto.getFirstName; if(/* dto.getLastName is defined */) p.setLastName = dto.getLastName; return ResponseEntity.ok(p); } } 

要求遗失财产

 {"firstName": "John"} 

预期行为:更新firstName= "John" (保留lastName不变)。

请求null属性

 {"firstName": "John", "lastName": null} 

预期行为:更新firstName="John"并设置lastName=null

我无法区分这两种情况,因为DTO中的lastName始终被Jackson设置为null

注意:我知道REST最佳实践(RFC 6902)建议使用PATCH而不是PUT进行部分更新,但在我的特定场景中,我需要使用PUT。

实际上,如果忽略validation,您可以像这样解决您的问题。

  public class BusDto { private Map changedAttrs = new HashMap<>(); /* getter and setter */ } 
  • 首先,为你的dto写一个超类,比如BusDto。
  • 其次,更改你的dto以扩展超类,并更改dto的set方法,将属性名称和值放入changedAttrs(因为当属性具有值无论null还是null时,spring将调用set)。
  • 第三,遍历地图。

使用布尔标志作为jackson的作者推荐 。

 class PersonDTO { private String firstName; private boolean isFirstNameDirty; public void setFirstName(String firstName){ this.firstName = firstName; this.isFirstNameDirty = true; } public void getFirstName() { return firstName; } public boolean hasFirstName() { return isFirstNameDirty; } } 

我试图解决同样的问题。 我发现使用JsonNode作为DTO非常容易。 这样您只能获得提交的内容。

您需要自己编写一个MergeService来执行实际工作,类似于BeanWrapper。 我还没有找到一个可以完全满足需要的现有框架。 (如果仅使用Json请求,则可以使用Jacksons readForUpdate方法。)

我们实际上使用另一种节点类型,因为我们需要“标准表单提交”和其他服务调用的相同function。 此外,修改应该在一个名为EntityService的事务中应用。

不幸的是,这个MergeService会变得非常复杂,因为你需要自己处理属性,列表,集合和映射:)

对我来说最有问题的部分是区分列表/集合的元素内的更改以及列表/集合的修改或替换。

而且validation也不容易,因为你需要针对另一个模型validation一些属性(在我的情况下是JPA实体)

编辑 – 一些映射代码(伪代码):

 class SomeController { @RequestMapping(value = { "/{id}" }, method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseBody public void save( @PathVariable("id") final Integer id, @RequestBody final JsonNode modifications) { modifierService.applyModifications(someEntityLoadedById, modifications); } } class ModifierService { public void applyModifications(Object updateObj, JsonNode node) throws Exception { BeanWrapperImpl bw = new BeanWrapperImpl(updateObj); Iterator fieldNames = node.fieldNames(); while (fieldNames.hasNext()) { String fieldName = fieldNames.next(); Object valueToBeUpdated = node.get(fieldName); Class propertyType = bw.getPropertyType(fieldName); if (propertyType == null) { if (!ignoreUnkown) { throw new IllegalArgumentException("Unkown field " + fieldName + " on type " + bw.getWrappedClass()); } } else if (Map.class.isAssignableFrom(propertyType)) { handleMap(bw, fieldName, valueToBeUpdated, ModificationType.MODIFY, createdObjects); } else if (Collection.class.isAssignableFrom(propertyType)) { handleCollection(bw, fieldName, valueToBeUpdated, ModificationType.MODIFY, createdObjects); } else { handleObject(bw, fieldName, valueToBeUpdated, propertyType, createdObjects); } } } } 

对于答案来说可能为时已晚,但您可以:

  • 默认情况下,不要取消设置“null”值。 通过查询参数提供一个明确的列表,您要取消设置哪些字段。 通过这种方式,您仍然可以发送与您的实体对应的JSON,并可以根据需要灵活地取消设置字段。

  • 根据您的使用情况,某些端点可能会将所有空值显式视为未设置操作。 修补有点危险,但在某些情况下可能是一种选择。