我试图在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<Person, Long> {
}
我使用的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。
实际上,如果忽略验证,您可以像这样解决您的问题。
public class BusDto {
private Map<String, Object> changedAttrs = new HashMap<>();
/* getter and setter */
}
使用布尔标志作为jackson's author recommends。
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;
}
}
另一种选择是使用java.util.Optional。
import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.Optional;
@JsonInclude(JsonInclude.Include.NON_NULL)
private class PersonDTO {
private Optional<String> firstName;
private Optional<String> lastName;
/* getters and setters ... */
}
如果未设置firstName,则该值为null,并且@JsonInclude批注将忽略该值。否则,如果在请求对象中隐式设置,firstName将不为null,但firstName.get()将为null。我发现这浏览解决方案@laffuste链接到little lower down in a different comment(garretwilson的初步评论说它没有工作结果工作)。
您还可以使用Jackson的ObjectMapper将DTO映射到实体,它将忽略未在请求对象中传递的属性:
import com.fasterxml.jackson.databind.ObjectMapper;
class PersonController {
// ...
@Autowired
ObjectMapper objectMapper
@Transactional
@RequestMapping(path = "/{personId}", method = RequestMethod.PUT)
public ResponseEntity<?> update(
@PathVariable String personId,
@RequestBody PersonDTO dto
) {
Person p = people.findOne(personId);
objectMapper.updateValue(p, dto);
personRepository.save(p);
// return ...
}
}
使用java.util.Optional验证DTO也有一点不同。 It's documented here,但花了一段时间才找到:
// ...
import javax.validation.constraints.NotNull;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
// ...
private class PersonDTO {
private Optional<@NotNull String> firstName;
private Optional<@NotBlank @Pattern(regexp = "...") String> lastName;
/* getters and setters ... */
}
在这种情况下,firstName可能根本不设置,但如果设置,则在验证PersonDTO时可能不会设置为null。
//...
import javax.validation.Valid;
//...
public ResponseEntity<?> update(
@PathVariable String personId,
@RequestBody @Valid PersonDTO dto
) {
// ...
}
也值得一提的是,Optional的使用似乎备受争议,而且编写Lombok的维护者时不会支持它(参见this question for example)。这意味着对具有约束的可选字段的类使用lombok.Data/lombok.Setter不起作用(它尝试创建具有完整约束的setter),因此使用@ Setter / @Data会导致异常被抛出,因为setter和成员变量具有约束集。在没有Optional参数的情况下编写Setter似乎也更好,例如:
//...
import lombok.Getter;
//...
@Getter
private class PersonDTO {
private Optional<@NotNull String> firstName;
private Optional<@NotBlank @Pattern(regexp = "...") String> lastName;
public void setFirstName(String firstName) {
this.firstName = Optional.ofNullable(firstName);
}
// etc...
}
我试图解决同样的问题。我发现使用JsonNode
作为DTO非常容易。这样您只能获得提交的内容。
您将需要自己编写一个MergeService
来完成实际工作,类似于BeanWrapper。我还没有找到一个可以完全满足需要的现有框架。 (如果您只使用Json请求,则可以使用Jacksons readForUpdate
方法。)
我们实际上使用另一种节点类型,因为我们需要“标准表单提交”和其他服务调用的相同功能。此外,修改应该在一个名为EntityService
的事务中应用。
不幸的是,这个MergeService
会变得非常复杂,因为你需要自己处理属性,列表,集合和地图:)
对我来说最有问题的部分是区分列表/集合的元素内的更改以及列表/集合的修改或替换。
而且验证也不容易,因为你需要针对另一个模型验证一些属性(在我的情况下是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<String> 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);
}
}
}
}
也许来不及答案,但你可以: