Java Spring MVC修补程序方法:部分更新

Java Spring MVC修补程序方法:部分更新,java,json,spring,rest,spring-mvc,Java,Json,Spring,Rest,Spring Mvc,我有一个项目,我正在使用SpringMVC+Jackson构建REST服务。假设我有以下java实体 public class MyEntity { private Integer id; private boolean aBoolean; private String aVeryBigString; //getter & setters } 有时候,我只是想更新布尔值,我不认为仅仅更新一个简单的布尔值就可以发送整个对象及其大字符串。因此,我考虑使用补丁

我有一个项目,我正在使用SpringMVC+Jackson构建REST服务。假设我有以下java实体

public class MyEntity {
    private Integer id;
    private boolean aBoolean;
    private String aVeryBigString;
    //getter & setters
}
有时候,我只是想更新布尔值,我不认为仅仅更新一个简单的布尔值就可以发送整个对象及其大字符串。因此,我考虑使用补丁HTTP方法只发送需要更新的字段。因此,我在控制器中声明以下方法:

@RequestMapping(method = RequestMethod.PATCH)
public void patch(@RequestBody MyVariable myVariable) {
    //calling a service to update the entity
}
@PatchMapping(value = "/{id}")
public ResponseEntity<?> patch(@RequestBody Map<String, Object> updates, @PathVariable("id") String id)
{
    // updates now only contains keys for fields that was updated
    return ResponseEntity.ok("resource updated");
}
@PatchMapping(value = "{id}")
public HttpEntity<Map<String, Object>> updatePartial(@PathVariable Integer id, @RequestBody Map<String, Object> data) {
    return ResponseEntity.ok(questionService.updatePartial(id, data));
}
问题是:我如何知道哪些字段需要更新?例如,如果客户端只想更新布尔值,我将得到一个带有空“aVeryBigString”的对象。我怎么知道用户只想更新布尔值,但不想清空字符串

我通过构建自定义URL“解决”了这个问题。例如,以下URL:POST/myentities/1/aboolean/true将映射到只允许更新布尔值的方法。此解决方案的问题在于它不符合REST。我不想百分之百地遵从REST,但我不喜欢提供一个自定义URL来更新每个字段(特别是当我想更新几个字段时,它会导致问题)

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


那么,有没有一种优雅的方法来解决这个问题呢?

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

PATCH的要点是您没有发送整个实体表示,因此我不理解您对空字符串的评论。您必须处理一些简单的JSON,例如:

{ aBoolean: true }

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

Spring没有/不能使用
PATCH
来修补对象,因为您已经遇到了相同的问题:JSON反序列化程序创建了一个带有空字段的Java POJO

这意味着您必须为修补实体提供自己的逻辑(即,仅当使用
PATCH
而不是
POST
时)

要么您知道您只使用非基本类型,要么使用一些规则(空字符串为
null
,并不适用于所有人),要么您必须提供一个额外的参数来定义重写的值。
最后一个对我来说很好:JavaScript应用程序知道除了列表中的JSON主体之外,哪些字段已经更改并发送到服务器。例如,如果一个字段
说明
被命名为要更改(补丁),但在JSON正文中没有给出,则该字段被置空。

正确的方法是

请求示例如下:

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

[
  { "op": "replace", "path": "aBoolean", "value": true }
]

我这样解决了问题,因为我无法更改服务

public class Test {

void updatePerson(Person person,PersonPatch patch) {

    for (PersonPatch.PersonPatchField updatedField : patch.updatedFields) {
        switch (updatedField){

            case firstname:
                person.setFirstname(patch.getFirstname());
                continue;
            case lastname:
                person.setLastname(patch.getLastname());
                continue;
            case title:
                person.setTitle(patch.getTitle());
                continue;
        }

    }

}

public static class PersonPatch {

    private final List<PersonPatchField> updatedFields = new ArrayList<PersonPatchField>();

    public List<PersonPatchField> updatedFields() {
        return updatedFields;
    }

    public enum PersonPatchField {
        firstname,
        lastname,
        title
    }

    private String firstname;
    private String lastname;
    private String title;

    public String getFirstname() {
        return firstname;
    }

    public void setFirstname(final String firstname) {
        updatedFields.add(PersonPatchField.firstname);
        this.firstname = firstname;
    }

    public String getLastname() {
        return lastname;
    }

    public void setLastname(final String lastname) {
        updatedFields.add(PersonPatchField.lastname);
        this.lastname = lastname;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(final String title) {
        updatedFields.add(PersonPatchField.title);
        this.title = title;
    }
}
公共类测试{
void updatePerson(个人,个人补丁){
for(PersonPatch.PersonPatchField updatedField:patch.updatedFields){
开关(更新字段){
案例名:
person.setFirstname(patch.getFirstname());
继续;
案例姓氏:
person.setLastname(patch.getLastname());
继续;
案例名称:
person.setTitle(patch.getTitle());
继续;
}
}
}
公共静态类PersonPatch{
private final List updatedFields=new ArrayList();
公共列表updatedFields(){
返回更新的字段;
}
公共枚举PersonPatchField{
名字,
姓,
标题
}
私有字符串名;
私有字符串lastname;
私有字符串标题;
公共字符串getFirstname(){
返回名字;
}
public void setFirstname(最终字符串firstname){
add(PersonPatchField.firstname);
this.firstname=firstname;
}
公共字符串getLastname(){
返回姓氏;
}
public void setLastname(最终字符串lastname){
add(PersonPatchField.lastname);
this.lastname=lastname;
}
公共字符串getTitle(){
返回标题;
}
公共无效集合标题(最终字符串标题){
添加(PersonPatchField.title);
this.title=标题;
}
}
Jackson只在值存在时调用。
因此,您可以保存调用的setter。

这可能会很晚,但为了新手和遇到相同问题的人,让我向您分享我自己的解决方案

在我过去的项目中,为了简单起见,我只使用本机java映射。它将捕获所有新值,包括客户端显式设置为null的null值。此时,可以很容易地确定哪些java属性需要设置为null,而当您使用与域模型相同的POJO时,您将无法区分nguish客户端将哪些字段设置为null,哪些字段不包括在更新中,但默认情况下将为null

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

更新

你可以用这种泛型代码为你的服务创建一个抽象的超类,你必须使用Java泛型
var data = JSON.stringify({
                aBoolean: true
            });
$.ajax({
    type: 'patch',
    contentType: 'application/json-patch+json',
    url: '/myentities/' + entity.id,
    data: data
});
@PatchMapping(value = "/{id}")
public ResponseEntity<?> patch(@RequestBody Map<String, Object> updates, @PathVariable("id") String id)
{
    // updates now only contains keys for fields that was updated
    return ResponseEntity.ok("resource updated");
}
import org.springframework.data.rest.webmvc.mapping.Associations

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

}
public class MyEntityUpdate {
    private Optional<String> aVeryBigString;
}
if(update.getAVeryBigString() != null)
    entity.setAVeryBigString(update.getAVeryBigString().get());
{
   "claimId":7,
   "claimTypeId": 1,
   "claimStatus": null
}
{
   "claimId": 7,
   "claimSrcAcctId": 12345678,
   "claimTypeId": 1,
   "claimDescription": "The vehicle is damaged beyond repair",
   "claimDateSubmitted": "2019-01-11 17:43:43",
   "claimStatus": null,
   "claimDateUpdated": "2019-04-09 13:43:07",
   "claimAcctAddress": "123 Sesame St, Charlotte, NC 28282",
   "claimContactName": "Steve Smith",
   "claimContactPhone": "777-555-1111",
   "claimContactEmail": "steve.smith@domain.com",
   "claimWitness": true,
   "claimWitnessFirstName": "Stan",
   "claimWitnessLastName": "Smith",
   "claimWitnessPhone": "777-777-7777",
   "claimDate": "2019-01-11 17:43:43",
   "claimDateEnd": "2019-01-11 12:43:43",
   "claimInvestigation": null,
   "scoring": null
}
@PatchMapping(value = "{id}")
public HttpEntity<Map<String, Object>> updatePartial(@PathVariable Integer id, @RequestBody Map<String, Object> data) {
    return ResponseEntity.ok(questionService.updatePartial(id, data));
}
public Map<String, Object> updatePartial(@NotNull Long id, @NotNull Map<String, Object> data) {

    Post post = postRepository.findById(id);

    Field[] postFields = Post.class.getDeclaredFields();
    HashMap<String, Object> toReturn = new HashMap<>(1);
    for (Field postField : postFields) {
        data.forEach((key, value) -> {
            if (key.equalsIgnoreCase(postField.getName())) {
                try {
                    final Field declaredField = Post.class.getDeclaredField(key);
                    declaredField.setAccessible(true);
                    declaredField.set(post, value);
                    toReturn.put(key, value);
                } catch (NoSuchFieldException | IllegalAccessException e) {
                    log.error("Unable to do partial update field: " + key + " :: ", e);
                    throw new BadRequestException("Something went wrong at server while partial updation");
                }
            }
        });
    }
    postRepository.save(post);

    return toReturn;
}
{
  voted: true,
  reported: true
}
if (response.data.hasOwnProperty("voted")){
  //do Something
} else{
  //do something e.g report it
}
@Mapper(componentModel = "spring")
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface CustomerMapper {
    void updateCustomerFromDto(CustomerDto dto, @MappingTarget Customer entity);
}

public void updateCustomer(CustomerDto dto) {
    Customer myCustomer = repo.findById(dto.id);
    mapper.updateCustomerFromDto(dto, myCustomer);
    repo.save(myCustomer);
}
@JsonDeserialize(using = Deser.class)
interface Changes {

    default boolean changed(String name) {
        Set<String> changed = changes();
        return changed != null && changed.contains(name);
    }

    void changes(Set<String> changed);

    Set<String> changes();
}
class Deser extends JsonDeserializer<Object> implements ContextualDeserializer {
    private Class<?> targetClass;

    public Deser() {}

    public Deser(Class<?> targetClass) { this.targetClass = targetClass; }

    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        ObjectMapper mapper = (ObjectMapper) p.getCodec();
        TypeReference<HashMap<String, Object>> typeRef = new TypeReference<>() {
        };
        HashMap<String, Object> map = p.readValueAs(typeRef);
        ObjectMapper innerMapper = mapper.copy();
        innerMapper.addMixIn(targetClass, RevertDefaultDeserialize.class);
        Object o = innerMapper.convertValue(map, targetClass);
        // this will only work with simple json->bean property mapping
        ((Changes) o).changes(map.keySet());
        return o;
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
        Class<?> targetClass = ctxt.getContextualType().getRawClass();
        return new Deser(targetClass);
    }

    @JsonDeserialize
    interface RevertDefaultDeserialize {
    }
}
@Data
class MyEntity implements Changes {
    private Integer id;
    private boolean aBoolean;
    private String aVeryBigString;

    @Getter(AccessLevel.NONE)
    @Setter(AccessLevel.NONE)
    private Set<String> changes;

    @Override
    public void changes(Set<String> changed) {
        this.changes = changed;
    }

    @Override
    public Set<String> changes() {
        return changes;
    }
}
class HowToUseIt {
    public static void example(MyEntity bean) {
        if (bean.changed("id")) {
            Integer id = bean.getId();
            // ...
        }
        if (bean.changed("aBoolean")) {
            boolean aBoolean = bean.isABoolean();
            // ...
        }
        if (bean.changed("aVeryBigString")) {
            String aVeryBigString = bean.getAVeryBigString();
            // ...
        }
    }
}
@Modifying
@Query("update Customer u set u.phone = :phone where u.id = :id")
void updatePhone(@Param(value = "id") long id, @Param(value = "phone") String phone);