Spring MVC PATCH方法:部分更新

mae*_*ael 46 java rest spring json spring-mvc

我有一个项目,我使用Spring MVC + Jackson来构建REST服务.假设我有以下java实体

public class MyEntity {
    private Integer id;
    private boolean aBoolean;
    private String aVeryBigString;
    //getter & setters
}
Run Code Online (Sandbox Code Playgroud)

有时候,我只是想更新布尔值,我不认为用更大的字符串发送整个对象只是为了更新一个简单的布尔值.所以,我考虑过使用PATCH HTTP方法只发送需要更新的字段.所以,我在我的控制器中声明了以下方法:

@RequestMapping(method = RequestMethod.PATCH)
public void patch(@RequestBody MyVariable myVariable) {
    //calling a service to update the entity
}
Run Code Online (Sandbox Code Playgroud)

问题是:我如何知道哪些字段需要更新?例如,如果客户端只想更新布尔值,我将得到一个空的"aVeryBigString"对象.我怎么知道用户只想更新布尔值,但不想清空字符串?

我通过构建自定义URL"解决"了这个问题.例如,以下URL:POST/myentities/1/aboolean/true将映射到只允许更新布尔值的方法.此解决方案的问题在于它不符合REST.我不希望100%兼容REST,但我不愿意提供自定义URL来更新每个字段(特别是考虑到当我想更新多个字段时它会导致问题).

另一个解决方案是将"MyEntity"拆分为多个资源并只更新这些资源,但我觉得它没有意义:"MyEntity" 一个普通资源,它不是其他资源组成的.

那么,有一种解决这个问题的优雅方式吗?

vin*_*ine 16

这可能很晚,但为了新手和遇到同样问题的人,让我分享一下我自己的解决方案.

在我过去的项目中,为了简单起见,我只使用原生的Java Map.它将捕获所有新值,包括客户端显式设置为null的空值.此时,很容易确定哪些java属性需要设置为null,与使用相同的POJO作为域模型不同,您将无法区分客户端设置的哪些字段为null和它们不包含在更新中,但默认情况下为null.

此外,您必须要求http请求发送要更新的记录的ID,并且不要将其包含在修补程序数据结构中.我所做的是,将URL中的ID设置为路径变量,将补丁数据设置为PATCH主体.然后使用ID,您将首先通过域模型获取记录,然后使用HashMap,您可以使用映射器服务或实用程序,用于修补相关域模型的更改.

更新

您可以使用这种通用代码为您的服务创建抽象超类,您必须使用Java Generics.这只是可能实现的一部分,我希望你能得到这个想法.使用Orika或Dozer等mapper框架也更好.

public abstract class AbstractService<Entity extends BaseEntity, DTO extends BaseDto> {
    @Autowired
    private MapperService mapper;

    @Autowired
    private BaseRepo<Entity> repo;

    private Class<DTO> dtoClass;

    private Class<Entity> entityCLass;

    public AbstractService(){
       entityCLass = (Class<Entity>) SomeReflectionTool.getGenericParameter()[0];
       dtoClass = (Class<DTO>) SomeReflectionTool.getGenericParameter()[1];
    }

    public DTO patch(Long id, Map<String, Object> patchValues) {
        Entity entity = repo.get(id);
        DTO dto = mapper.map(entity, dtoClass);
        mapper.map(patchValues, dto);
        Entity updatedEntity = toEntity(dto);
        save(updatedEntity);
        return dto;
    }
}
Run Code Online (Sandbox Code Playgroud)


Che*_*pir 10

正确的方法是在JSON PATCH RFC 6902中提出的方法

请求示例如下:

PATCH http://example.com/api/entity/1 HTTP/1.1
Content-Type: application/json-patch+json 

[
  { "op": "replace", "path": "aBoolean", "value": true }
]
Run Code Online (Sandbox Code Playgroud)

  • 在一篇文章中,作者称那些不同意他的人是"白痴"并不能为我做很多事情.修补{"email":"foo@bar.com"}以更新某些内容并没有错.这是更新服务器上信息的最简洁形式,我认为它完全符合RF5789.它是更新的完全封装表示.JSON PATCH在大多数情况下都是过度杀伤,并没有解决原始海报试图做或暗示的内容. (10认同)
  • 这是错误的补丁.有JSON补丁和HTTP补丁(这是一个动词,如get,post,put等).https://tools.ietf.org/html/rfc5789 (4认同)
  • @EricBrandel你为什么说这是错的?上面的示例使用了两个:[RFC 5789](http://tools.ietf.org/html/rfc5789)中定义的PATCH HTTP方法,以及[在[中定义的] json补丁数据格式(application/json-patch + json) RFC 6902](http://tools.ietf.org/html/rfc6902).此外,HTTP PATCH方法规范提到*[...]封闭实体包含一组指令,描述如何修改当前驻留在源服务器上的资源以生成新版本.*这意味着使用明确定义操作的数据格式,哪个应用程序/ json-patch + json (2认同)

sno*_*lli 7

深入研究后,我发现了一种可接受的解决方案,使用的是Spring MVC当前使用的相同方法,DomainObjectReader另请参见:JsonPatchHandler

@RepositoryRestController
public class BookCustomRepository {
    private final DomainObjectReader domainObjectReader;
    private final ObjectMapper mapper;

    private final BookRepository repository;


    @Autowired
    public BookCustomRepository(BookRepository bookRepository, 
                                ObjectMapper mapper,
                                PersistentEntities persistentEntities,
                                Associations associationLinks) {
        this.repository = bookRepository;
        this.mapper = mapper;
        this.domainObjectReader = new DomainObjectReader(persistentEntities, associationLinks);
    }


    @PatchMapping(value = "/book/{id}", consumes = {MediaType.APPLICATION_JSON_UTF8_VALUE, MediaType.APPLICATION_JSON_VALUE})
    public ResponseEntity<?> patch(@PathVariable String id, ServletServerHttpRequest request) throws IOException {

        Book entityToPatch = repository.findById(id).orElseThrow(ResourceNotFoundException::new);
        Book patched = domainObjectReader.read(request.getBody(), entityToPatch, mapper);
        repository.save(patched);

        return ResponseEntity.noContent().build();
    }

}
Run Code Online (Sandbox Code Playgroud)


Tom*_*m G 5

重点PATCH是您没有发送整个实体表示,所以我不理解您对空字符串的评论。您必须处理某种简单的 JSON,例如:

{ aBoolean: true }
Run Code Online (Sandbox Code Playgroud)

并将其应用到指定的资源。这个想法是,接收到的是所需资源状态和当前资源状态的差异。

  • 我知道 PATCH 的意义。JSON 部分不是问题。问题是 JSON 反序列化。在服务器端,我收到一个 Java 对象,而不是一个 JSON 字符串(因为 Spring MVC 的魔力,我想保留这个魔力)。如果我刚刚收到一个 JSON 字符串,我当然可以立即知道客户端发送了什么。通过这个简单的 JSON:`{ aBoolean: true }`,我收到了一个完整的“MyEntity”对象,其中“aVeryBigString”属性为空。问题是:我如何知道“aVeryBigString”属性是否被客户端清空或只是未发送? (9认同)

Mic*_*che 5

你可以用Optional<>它:

public class MyEntityUpdate {
    private Optional<String> aVeryBigString;
}
Run Code Online (Sandbox Code Playgroud)

这样您就可以按如下方式检查更新对象:

if(update.getAVeryBigString() != null)
    entity.setAVeryBigString(update.getAVeryBigString().get());
Run Code Online (Sandbox Code Playgroud)

如果aVeryBigStringJSON 文档中没有该字段,则 POJOaVeryBigString字段将为null. 如果它在 JSON 文档中,但带有null值,则 POJO 字段将是Optional带有包装值的null。该解决方案允许您区分“无更新”和“设置为空”的情况。

  • 尽管 Java 的可选值并不打算用作字段,但这对我来说仍然是最直接的解决方案,并且是非常有用的完美案例,即使不是故意的。 (3认同)

mvb*_*b13 -20

您可以将布尔值更改为布尔值,并为您不想更新的所有字段分配空值。唯一一个非空值将定义客户端要更新的字段。

  • 否决是因为发送带有空值属性的“PATCH”与发送不包含属性值的“PATCH”本质上不同(例如,对于遵守 JSONAPI 规范的应用程序,我希望前者*取消设置*属性,然后保持属性不变)。 (5认同)
  • PATCH 应该仅用于发送要更新的属性..根据 http://stackoverflow.com/a/20655696/986160 和 http://williamdurand.fr/2014/02/14/please-do-not -像白痴一样的补丁/ (4认同)