如何区分Spring Rest Controller中部分更新的null值和未提供值

问题描述 投票:20回答:5

我试图在Spring Rest Controller中使用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 ... */
}

我的人员库(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:

@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不变)。

请求null属性

{"firstName": "John", "lastName": null}

预期行为:更新firstName="John"并设置lastName=null

我无法区分这两种情况,因为DTO中的lastName总是由Jackson设置为null

注意:我知道REST最佳实践(RFC 6902)建议使用PATCH而不是PUT进行部分更新,但在我的特定场景中,我需要使用PUT。

java json rest spring-mvc jackson
5个回答
3
投票

实际上,如果忽略验证,您可以像这样解决您的问题。

   public class BusDto {
       private Map<String, Object> changedAttrs = new HashMap<>();

       /* getter and setter */
   }
  • 首先,为你的dto写一个超类,比如BusDto。
  • 其次,更改你的dto以扩展超类,并更改dto的set方法,将属性名称和值放入changedAttrs(因为当属性具有值无论null还是null时,spring将调用set)。
  • 第三,遍历地图。

6
投票

使用布尔标志作为jackson's author recommends

class PersonDTO {
    private String firstName;
    private boolean isFirstNameDirty;

    public void setFirstName(String firstName){
        this.firstName = firstName;
        this.isFirstNameDirty = true;
    }

    public void getFirstName() {
        return firstName;
    }

    public boolean hasFirstName() {
        return isFirstNameDirty;
    }
}

3
投票

另一种选择是使用java.util.Optional。

import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.Optional;

@JsonInclude(JsonInclude.Include.NON_NULL)
private class PersonDTO {
    private Optional<String> firstName;
    private Optional<String> lastName;
    /* getters and setters ... */
}

如果未设置firstName,则该值为null,并且@JsonInclude批注将忽略该值。否则,如果在请求对象中隐式设置,firstName将不为null,但firstName.get()将为null。我发现这浏览解决方案@laffuste链接到little lower down in a different comment(garretwilson的初步评论说它没有工作结果工作)。

您还可以使用Jackson的ObjectMapper将DTO映射到实体,它将忽略未在请求对象中传递的属性:

import com.fasterxml.jackson.databind.ObjectMapper;

class PersonController {
    // ...
    @Autowired
    ObjectMapper objectMapper

    @Transactional
    @RequestMapping(path = "/{personId}", method = RequestMethod.PUT)
    public ResponseEntity<?> update(
            @PathVariable String personId,
            @RequestBody PersonDTO dto
    ) {
        Person p = people.findOne(personId);
        objectMapper.updateValue(p, dto);
        personRepository.save(p);
        // return ...
    }
}

使用java.util.Optional验证DTO也有一点不同。 It's documented here,但花了一段时间才找到:

// ...
import javax.validation.constraints.NotNull;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
// ...
private class PersonDTO {
    private Optional<@NotNull String> firstName;
    private Optional<@NotBlank @Pattern(regexp = "...") String> lastName;
    /* getters and setters ... */
}

在这种情况下,firstName可能根本不设置,但如果设置,则在验证PersonDTO时可能不会设置为null。

//...
import javax.validation.Valid;
//...
public ResponseEntity<?> update(
        @PathVariable String personId,
        @RequestBody @Valid PersonDTO dto
) {
    // ...
}

也值得一提的是,Optional的使用似乎备受争议,而且编写Lombok的维护者时不会支持它(参见this question for example)。这意味着对具有约束的可选字段的类使用lombok.Data/lombok.Setter不起作用(它尝试创建具有完整约束的setter),因此使用@ Setter / @Data会导致异常被抛出,因为setter和成员变量具有约束集。在没有Optional参数的情况下编写Setter似乎也更好,例如:

//...
import lombok.Getter;
//...
@Getter
private class PersonDTO {
    private Optional<@NotNull String> firstName;
    private Optional<@NotBlank @Pattern(regexp = "...") String> lastName;

    public void setFirstName(String firstName) {
        this.firstName = Optional.ofNullable(firstName);
    }
    // etc...
}

1
投票

我试图解决同样的问题。我发现使用JsonNode作为DTO非常容易。这样您只能获得提交的内容。

您将需要自己编写一个MergeService来完成实际工作,类似于BeanWrapper。我还没有找到一个可以完全满足需要的现有框架。 (如果您只使用Json请求,则可以使用Jacksons readForUpdate方法。)

我们实际上使用另一种节点类型,因为我们需要“标准表单提交”和其他服务调用的相同功能。此外,修改应该在一个名为EntityService的事务中应用。

不幸的是,这个MergeService会变得非常复杂,因为你需要自己处理属性,列表,集合和地图:)

对我来说最有问题的部分是区分列表/集合的元素内的更改以及列表/集合的修改或替换。

而且验证也不容易,因为你需要针对另一个模型验证一些属性(在我的情况下是JPA实体)

编辑 - 一些映射代码(伪代码):

class SomeController { 
   @RequestMapping(value = { "/{id}" }, method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public void save(
            @PathVariable("id") final Integer id,
            @RequestBody final JsonNode modifications) {
        modifierService.applyModifications(someEntityLoadedById, modifications);
    }
}

class ModifierService {

    public void applyModifications(Object updateObj, JsonNode node)
            throws Exception {

        BeanWrapperImpl bw = new BeanWrapperImpl(updateObj);
        Iterator<String> fieldNames = node.fieldNames();

        while (fieldNames.hasNext()) {
            String fieldName = fieldNames.next();
            Object valueToBeUpdated = node.get(fieldName);
            Class<?> propertyType = bw.getPropertyType(fieldName);
            if (propertyType == null) {
               if (!ignoreUnkown) {
                    throw new IllegalArgumentException("Unkown field " + fieldName + " on type " + bw.getWrappedClass());
                }
            } else if (Map.class.isAssignableFrom(propertyType)) {
                    handleMap(bw, fieldName, valueToBeUpdated, ModificationType.MODIFY, createdObjects);
            } else if (Collection.class.isAssignableFrom(propertyType)) {
                    handleCollection(bw, fieldName, valueToBeUpdated, ModificationType.MODIFY, createdObjects);
            } else {
                    handleObject(bw, fieldName, valueToBeUpdated, propertyType, createdObjects);
            }
        }
    }
}

0
投票

也许来不及答案,但你可以:

  • 默认情况下,不要取消设置'null'值。通过查询参数提供一个明确的列表,您要取消设置哪些字段。通过这种方式,您仍然可以发送与您的实体对应的JSON,并可以根据需要灵活地取消设置字段。
  • 根据您的使用情况,某些端点可能会将所有空值显式视为未设置操作。修补有点危险,但在某些情况下可能是一种选择。
© www.soinside.com 2019 - 2024. All rights reserved.