如何在 Spring Rest 控制器中区分部分更新的 null 值和未提供的值

2022-08-31 13:52:04

我试图在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 ... */
}

My Person repository (Spring Data):

@Repository
public interface PersonRepository extends CrudRepository<Person, Long> {
}

我使用的 DTO:

private class PersonDTO {
    private String firstName;
    private String lastName;

    /* getters and setters ... */
}

我的春季休息控制器:

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

预期行为:更新名字 = “John”(保留姓氏不变)。

具有空属性的请求

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

预期行为:更新 firstName=“John” 并将 lastName 设置为 null

我无法区分这两种情况,因为在DTO中总是由Jackson设置。lastNamenull

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


答案 1

另一种选择是使用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链接到另一条评论中的下半部分(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也略有不同。它记录在这里,但我花了一段时间才找到:

// ...
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 ... */
}

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

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

另外可能值得一提的是,Optional的使用似乎备受争议,并且在撰写本文时,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...
}

答案 2

使用布尔标志,正如杰克逊的作者所建议的那样

class PersonDTO {
    private String firstName;
    private boolean isFirstNameDirty;

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

    public String getFirstName() {
        return firstName;
    }

    public boolean hasFirstName() {
        return isFirstNameDirty;
    }
}