我想通过发送空请求来为实体设置空值。
例如:
PATCH: "{deleteDate: null}" to http://localhost/api/entity/1
但这不起作用。
我在here找到了如何处理 PATCH 请求的信息:
- 创建了 Foo 的新实例
- Foo 填充了随请求发送的所有值
- 具有 URI 提供的 id 的 Foo 实体已加载
- 两个对象之间不同的所有属性都会从新的 Foo 复制到持久化的 Foo,除非新的 Foo 中的值为 null。
我是否正确理解,不可能通过对 spring-data-rest 服务 API 的 PATCH 请求将值设置为 NULL?
在 Spring 上下文中,PATCH 方法中的 null 值意味着没有更改。 如果你想写空值,你可以
1)使用PUT方法;
2) 实现您自己的 DomainObjectMerger 类,您可以在其中扩展方法合并,条件如下
sourceValue != targetValue;
3) 使用 DomainObjectMerger.NullHandlingPolicy 配置。
取决于您的 Spring Data REST 版本。
egorlitvinenko 的回答中的所有 3 个选项都将解决所描述的问题,但还会有另一个:
PATCH request
中未指定的所有其他属性也将被“无效”。似乎 spring-data-rest,issue-345 已在
v2.2.x
中修复。
可以使用Map
控制器示例:
@PatchMapping("api/entity/{entityId}")
ResponseEntity<EntityDto> modifyEntity(@PathVariable UUID entityId,
@RequestBody Map<String, Object> modifyEntityData) {
EntityDto modifiedEntity = entityService
.modifyEntity(entityId, modifyEntityData);
return ResponseEntity
.status(HttpStatus.OK)
.body(modifiedEntity );
}
服务示例:
@Transactional
public EntityDto modifyEntity(UUID entityId, Map<String, Object> modifyEntityData) {
// I assume you have a method that returns Entity by its Id in service
Entity entityToModify = findEntity(entityId);
entityToModify = applyChangesToEntity(entityToModify, modifyEntityData);
// And mapper that returns dto (ofc optional)
return EntityMapper.mapEntityToDto(entityToModify);
}
private EntityDto applyChangesToEntity(Entity exisistingEntity,
Map<String, Object> modifyEntityData) {
modifyEntityData.forEach((key, value) -> {
switch (key) {
case "deleteDate" -> {
// Check if null
if (value == null) {
exisistingEntity.setDeleteDate(null);
break;
}
// If value is present you can do w/e you want
if (validateValueOk(value)) {
exisistingEntity.setDeleteDate(value.toString());
} else {
throw new RuntimeException("DeleteDate must be valid or other message");
}
}
// HERE MORE CASES
default -> throw new RuntimeException("Message");
}
});
return exisistingEntity;
}
作为 Łukasz Mączka 解决方案的替代方案,如果您仍想在控制器中接收
Foo
的实例,您可以创建一个 Deserializer 类,使用反射作为通用方法。
import com.fasterxml.jackson.databind.JsonDeserializer;
public class FooDeserializer extends JsonDeserializer<Foo> {
@Autowired
private FooService service;
@Override
public Foo deserialize(JsonParser p, DeserializationContext ctxt) {
ObjectMapper objectMapper = (ObjectMapper) p.getCodec();
JsonNode patchNode = objectMapper.readTree(p);
// If editting, start with entity stored on db, otherwise create a new one
Long id = extractEntityIdFromPathVariable();
Foo instance = id!=null ? service.findById(id) : new Foo();
if (instance==null) throw new EntityNotFoundException();
// For every field sent in the payload... (fields not sent are not considered)
patchNode.fields().forEachRemaining(entry -> {
String fieldName = entry.getKey();
JsonNode valueNode = entry.getValue();
Field f = Foo.class.getDeclaredField(fieldName);
if (valueNode instanceof NullNode) { // If value of field in the payload is null, set it to null in the instance
// THIS IS THE SOLUTION TO YOUR PROBLEM :)
f.set(instance, (Object)null);
} else { // Otherwise, map it to the correspondant type and set it in the instance
f.set(instance, new ObjectMapper().treeToValue(valueNode, f.getType()));
}
})
}
private Long extractEntityIdFromPathVariable() {
// Extract the entity ID from the URL path variable
String pathInfo = request.getRequestURI();
if (pathInfo != null) {
String[] pathSegments = pathInfo.split("/");
if (pathSegments.length > 1) {
try {
return Long.parseLong(pathSegments[pathSegments.length-1]);
} catch (NumberFormatException e) {
// Handle the case where the path variable is not a valid I
}
}
}
// Handle the case where the ID is not found in the path
return null;
}
}
这里有一些例外情况需要处理。我没有包含它们是为了简化代码。
然后,将其定义为
Foo
类中的默认序列化器:
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
@JsonDeserialize(using = FooDeserializer.class)
public class Foo {
protected Date deleteDate;
...
}
确保您正在编辑的字段是
或public
(最后一个,模型和反序列化器类必须位于同一个包中)。否则该套件将无法工作。protected
作为替代方案,您可以通过
获取java.lang.reflect.Method
,使用Foo.class.getMethod()
将String.format()
与方法名称合并(可能在其前面添加“set”)。fieldName
最后,在控制器方法中,您只需调用存储库上的 save 方法,因为解串器是在控制器方法之前调用的,因此您到达那里的参数已经具有“已清理”版本。 :)
public FooController {
@PatchMapping("{id}")
public Foo fooEdit(@RequestBody Foo foo) {
return fooRepository.save(foo);
}
}