我试图在SpringRest控制器中使用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 ... */
}
My Person存储库(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
@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
不变)。*
具有空属性的请求
{"firstName": "John", "lastName": null}
- 预期行为:更新
firstName="John"
并设置lastName=null
。*
我无法区分这两种情况,因为Jackson总是将DTO中的lastName
设置为null
。
- 注意:我知道REST最佳实践(RFC 6902)建议使用PATCH而不是PUT进行部分更新,但在我的特定场景中,我需要使用PUT。*
8条答案
按热度按时间1sbrub3j1#
另一个选择是使用java. util. Optional。
如果没有设置firstName,则该值为null,并且会被@JsonInclude注解忽略。否则,如果在request对象中隐式设置了firstName,则firstName不会为null,但firstName.get()会为null。我在浏览解决方案@laffuste时发现了这一点,该解决方案在另一个注解中链接到了下面一点(garretwilson最初的注解说它不起作用,结果却起作用了)。
您还可以使用Jackson的ObjectMapper将DTOMap到实体,它将忽略请求对象中未传递的属性:
使用java.util.Optional验证DTO也有一些不同。这里有文档,但是我花了一些时间才找到:
在这种情况下,firstName可能根本不设置,但如果设置了firstName,并且PersonDTO已验证,则可能不设置为null。
另外值得一提的是,Optional的使用似乎存在很大的争议,在编写Lombok的维护者时不支持(参见this question for example)。这意味着在具有带约束的可选字段的类上使用lombok.Data/lombok.Setter不起作用(它试图创建具有完整约束的setter),因此使用@Setter/@Data会引发异常,因为setter和成员变量都设置了约束。编写不带可选参数的Setter似乎也更好,例如:
yxyvkwin2#
有一个更好的选择,它不涉及更改您的DTO的或自定义您的设置器。
它包括让Jackson将数据与现有数据对象合并,如下所示:
newData
中不存在的任何字段都不会覆盖existingData
中的数据,但如果字段存在,则即使它包含null
,也会被覆盖。演示代码:
结果以
{"text": "patched text", "address": "address", "city": null}
表示请注意,
text
和city
已修补(city
现在是null
),而address
则保持不变。在SpringRest控制器中,您需要获取原始JSON数据,而不是让Spring对其进行反序列化。
n3ipq98p3#
按照Jackson的作者的建议,使用布尔标志。
vxqlmq5t4#
实际上,如果忽略验证,你可以这样解决你的问题。
izkcnapc5#
我曾经尝试过解决同样的问题。我发现使用
JsonNode
作为DTO相当容易。这样你只会得到提交的内容。您需要自己编写一个
MergeService
来完成实际的工作,类似于BeanWrapper。我还没有找到一个现有的框架来完成所需的工作。(如果您只使用Json请求,您可能可以使用Jacksons的readForUpdate
方法。)我们实际上使用了另一种节点类型,因为我们需要从“标准表单提交”和其他服务调用中获得相同的功能。此外,修改应该应用于名为
EntityService
的事务中。不幸的是,这个
MergeService
将变得相当复杂,因为您需要自己处理属性、列表、集合和Map:)对我来说最有问题的部分是区分列表/集合的元素内的更改和列表/集合的修改或替换。
而且验证也不容易,因为您需要根据另一个模型(在我的例子中是JPA实体)验证一些属性。
EDIT -一些Map代码(伪代码):
ruarlubt6#
也许现在回答太晚了,但你可以:
cx6n0qe37#
另一个解决方案是强制反序列化请求主体。通过这样做,您将能够收集用户提供的字段并选择性地验证它们。
因此,您的DTO可能如下所示:
您的控制器可以是这样的:
不需要Optional's,不需要额外的依赖,你的正常验证就可以了,你的昂首阔步看起来不错。唯一的问题是,你没有得到嵌套对象的正确合并补丁,但在许多用例中,这甚至是不需要的。
ppcbkaq58#
可能太晚了,但是下面的代码可以区分空值和未提供的值