在Spring Rest Controller中使用PUT请求方法部分更新实体时,我试图区分空值和未提供的值。
以下面的实体为例:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
End Sub
我的人员存储库(Spring Data):
@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 ... */
}
我使用的DTO:
@Repository
public interface PersonRepository extends CrudRepository<Person, Long> {
}
My Spring RestController:
private class PersonDTO {
private String firstName;
private String lastName;
/* getters and setters ... */
}
缺少财产的请求
@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"
不变)。
请求使用null属性
lastName
预期行为:更新{"firstName": "John", "lastName": null}
并设置firstName="John"
。
我无法区分这两种情况,因为DTO中的lastName=null
始终被杰克逊设为lastName
。
注意: 我知道REST最佳实践(RFC 6902)建议使用PATCH而不是PUT进行部分更新,但在我的特定场景中,我需要使用PUT。
答案 0 :(得分:6)
将布尔标志用作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;
}
}
答案 1 :(得分:5)
还有一个更好的选择,它不涉及更改DTO或自定义设置器。
它涉及让Jackson将数据与现有数据对象合并,如下所示:
MyData existingData = ...
ObjectReader readerForUpdating = objectMapper.readerForUpdating(existingData);
MyData mergedData = readerForUpdating.readValue(newData);
newData
中不存在的任何字段都不会覆盖existingData
中的数据,但是如果存在一个字段,即使其中包含null
,该字段也会被覆盖。
演示代码:
ObjectMapper objectMapper = new ObjectMapper();
MyDTO dto = new MyDTO();
dto.setText("text");
dto.setAddress("address");
dto.setCity("city");
String json = "{\"text\": \"patched text\", \"city\": null}";
ObjectReader readerForUpdating = objectMapper.readerForUpdating(dto);
MyDTO merged = readerForUpdating.readValue(json);
结果为{"text": "patched text", "address": "address", "city": null}
在Spring Rest Controller中,您将需要获取原始JSON数据,而不是让Spring反序列化此数据。因此,如下更改端点:
@Autowired ObjectMapper objectMapper;
@RequestMapping(path = "/{personId}", method = RequestMethod.PATCH)
public ResponseEntity<?> update(
@PathVariable String personId,
@RequestBody JsonNode jsonNode) {
RequestDto existingData = getExistingDataFromSomewhere();
ObjectReader readerForUpdating = objectMapper.readerForUpdating(existingData);
RequestDTO mergedData = readerForUpdating.readValue(jsonNode);
...
)
答案 2 :(得分:3)
实际上,如果忽略验证,您可以像这样解决问题。
public class BusDto {
private Map<String, Object> changedAttrs = new HashMap<>();
/* getter and setter */
}
答案 3 :(得分:3)
另一种选择是使用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。我发现这是浏览链接到little lower down in a different comment的解决方案@laffuste(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)。这意味着在具有带有约束的Optional字段的类上使用lombok.Data/lombok.Setter无效(尝试创建具有完整约束的setter),因此使用@ Setter / @ Data会引发异常,因为setter和member变量已设置约束。写不带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...
}
答案 4 :(得分:1)
我试图解决同样的问题。我发现使用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);
}
}
}
}
答案 5 :(得分:0)
回答可能为时已晚,但你可以:
默认情况下,不要设置&#39; null&#39;值。通过查询参数提供一个明确的列表,您要取消设置哪些字段。通过这种方式,您仍然可以发送与您的实体相对应的JSON,并可以根据需要灵活地取消设置字段。
根据您的使用情况,某些端点可能会明确将所有空值视为未设置操作。修补有点危险,但在某些情况下可能是一种选择。
答案 6 :(得分:-1)
可能到最近,但以下代码对我有用,以区分null和未提供的值
if(dto.getIban() == null){
log.info("Iban value is not provided");
}else if(dto.getIban().orElse(null) == null){
log.info("Iban is provided and has null value");
}else{
log.info("Iban value is : " + dto.getIban().get());
}