Spring data-rest:通过 PATCH 请求设置 null 值

问题描述 投票:0回答:4

我想通过发送空请求来为实体设置空值。

例如:

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?

java spring rest spring-data
4个回答
9
投票

在 Spring 上下文中,PATCH 方法中的 null 值意味着没有更改。 如果你想写空值,你可以

1)使用PUT方法;
2) 实现您自己的 DomainObjectMerger 类,您可以在其中扩展方法合并,条件如下

sourceValue != targetValue;

3) 使用 DomainObjectMerger.NullHandlingPolicy 配置。
取决于您的 Spring Data REST 版本。


1
投票

egorlitvinenko 的回答中的所有 3 个选项都将解决所描述的问题,但还会有另一个:

  • PATCH request
    中未指定的所有其他属性也将被“无效”。

似乎 spring-data-restissue-345 已在

v2.2.x
中修复。


0
投票

可以使用Map作为请求体来实现,然后你可以在map上使用forEach提取数据。

控制器示例:

@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;
}

0
投票

作为 Ł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()
fieldName
与方法名称合并(可能在其前面添加“set”)。

最后,在控制器方法中,您只需调用存储库上的 save 方法,因为解串器是在控制器方法之前调用的,因此您到达那里的参数已经具有“已清理”版本。 :)

public FooController {
  @PatchMapping("{id}")
  public Foo fooEdit(@RequestBody Foo foo) {
    return fooRepository.save(foo);
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.