当前位置: 首页 > 知识库问答 >
问题:

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

濮俊美
2023-03-14

当在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

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

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

共有3个答案

叶健柏
2023-03-14

还有一个更好的选择,不涉及更改DTO或自定义设置程序。

它包括让Jackson将数据与现有数据对象合并,如下所示:

MyData existingData = ...
ObjectReader readerForUpdating = objectMapper.readerForUpdating(existingData);

MyData mergedData = readerForUpdating.readValue(newData);    

newData中不存在的任何字段都不会覆盖existingData中的数据,但如果存在字段,则会覆盖该字段,即使该字段包含null

演示代码

    ObjectMapper objectMapper = new ObjectMapper();
    MyDTO dto = new MyDTO();

    dto.setText("text");
    dto.setAddress("address");
    dto.setCity("city");

    String json = "{\"text\": \"patched text\", \"city\": null}";

    ObjectReader readerForUpdating = objectMapper.readerForUpdating(dto);

    MyDTO merged = readerForUpdating.readValue(json);

结果为{文本:修补文本,地址:地址,城市:空}

在SpringREST控制器中,您需要获取原始JSON数据,而不是让Spring进行反序列化。因此,按如下方式更改endpoint:

@Autowired ObjectMapper objectMapper;

@RequestMapping(path = "/{personId}", method = RequestMethod.PATCH)
public ResponseEntity<?> update(
        @PathVariable String personId,
        @RequestBody JsonNode jsonNode) {

   RequestDto existingData = getExistingDataFromSomewhere();

   ObjectReader readerForUpdating = objectMapper.readerForUpdating(existingData);
   
   RequestDTO mergedData = readerForUpdating.readValue(jsonNode);

   ...
)
蓬弘
2023-03-14

按照jackson的作者的建议使用布尔标志。

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;
    }
}
钱嘉致
2023-03-14

另一个选项是使用java.util.可选。

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()将是。我在浏览解决方案@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.验证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 ... */
}

在这种情况下,firstName可能根本没有被设置,但是如果设置了,如果验证了个人DTO,则可能不会设置为null。

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

还值得一提的是,可选的使用似乎是高度争论的,并且在撰写本文时,Lombok的维护者不支持它(例如,参见这个问题)。这意味着使用龙目岛。数据/龙目岛。类中具有带约束的可选字段的Setter不起作用(它试图创建约束完好无损的setter),因此使用@Setter/@Data会引发异常,因为setter和成员变量都设置了约束。编写没有可选参数的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...
}
 类似资料:
  • 问题内容: 我试图在Spring Rest Controller中使用PUT请求方法部分更新实体时区分空值和未提供值。 以以下实体为例: 我的人资料库(Spring数据): 我使用的DTO: 我的Spring RestController: 请求缺少属性 预期行为:更新(保留不变)。 具有空属性的请求 预期行为:更新并设置。 我无法区分这两种情况,因为在DTO中,总是由Jackson 设置。 注意

  • 本文向大家介绍区分MySQL中的空值(null)和空字符(''),包括了区分MySQL中的空值(null)和空字符('')的使用技巧和注意事项,需要的朋友参考一下 日常开发中,一般都会涉及到数据库增删改查,那么不可避免会遇到Mysql中的NULL和空字符。 空字符('')和空值(null)表面上看都是空,其实存在一些差异: 定义: 空值(NULL)的长度是NULL,不确定占用了多少存储空间,但是占

  • 我在一个结构中有一个固定大小的缓冲区,我想在它中间复制一些数据。 我现在唯一能看到的就是从开始的部分,加上我想要的,然后在最后加上部分,但是我确信这会导致一两个我想要避免的大副本,我只是需要更新中间的缓冲区。有没有一种简单的方法可以做到这一点而不使用不安全的代码?

  • 如何区分未发送的值和空值?如何检测客户端是否发送了空字段或跳过字段?

  • 问题内容: 让我们考虑以下情况-“文章”文档中有两个字段- content(string)和views(int)。视图字段未建立索引。views字段包含此文章被阅读了多少次的信息。 来自官方文档: 我们还说过文件是不可变的:它们不能更改,只能替换。更新API必须遵守相同的规则。从外部看,似乎我们正在部分更新文档。但是,在内部,更新API仅管理与我们已经描述过的相同的检索- 更改-重新索引过程。 但

  • 我正试图在应用程序处于退出状态时从firebase保存一个通知到redux。 但是我需要在app.js中这样做,当我甚至不在组件上时,我如何分派redux操作呢? 这就是我要做的