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

Dan*_*lli 27 java rest json spring-mvc jackson

我试图在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 ... */
}
Run Code Online (Sandbox Code Playgroud)

我的人员库(Spring Data):

@Repository
public interface PersonRepository extends CrudRepository<Person, Long> {
}
Run Code Online (Sandbox Code Playgroud)

我使用的DTO:

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

    /* getters and setters ... */
}
Run Code Online (Sandbox Code Playgroud)

我的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);
    }
}
Run Code Online (Sandbox Code Playgroud)

要求遗失财产

{"firstName": "John"}
Run Code Online (Sandbox Code Playgroud)

预期行为:更新firstName= "John"(lastName保持不变).

请求null属性

{"firstName": "John", "lastName": null}
Run Code Online (Sandbox Code Playgroud)

预期的行为:更新firstName="John"和设置lastName=null.

我无法区分这两种情况,因为lastNameDTO总是null由杰克逊设定.

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

joh*_*384 8

有一个更好的选择,它不涉及更改您的 DTO 或自定义您的设置器。

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

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

MyData mergedData = readerForUpdating.readValue(newData);    
Run Code Online (Sandbox Code Playgroud)

中不存在的任何字段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);
Run Code Online (Sandbox Code Playgroud)

结果是 {"text": "patched text", "address": "address", "city": null}

在 Spring Rest Controller 中,您需要获取原始 JSON 数据,而不是让 Spring 反序列化它来执行此操作。所以像这样改变你的端点:

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

   ...
)
Run Code Online (Sandbox Code Playgroud)

  • 为什么使用普通字符串作为 json?我认为你打破了这里的例子。您应该使用已解码的实体来回答。 (2认同)
  • @Sebastian我真的不明白你在这里问什么——为了演示它是如何工作的,我使用了一个字符串,有什么问题吗?请参阅 Spring 控制器的最后一个示例,那里没有 json 字符串。 (2认同)
  • @Michał Jabłoński 在任何情况下,我认为您需要在应用部分更新后重新验证合并的实体,因为可能存在修改和未修改字段的跨字段验证。 (2认同)

laf*_*ste 7

按照杰克逊作者的建议使用布尔标志。

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @Andrew 那么 GSON 如何解决这个问题呢? (5认同)
  • 这个解决方案有效,但我认为这是 Jackson 的一个失败,并导致大量代码膨胀......不使用它的充分理由。看起来 GSON 是一个不错的选择:https://github.com/google/gson/blob/master/UserGuide.md#TOC-Null-Object-Support (4认同)

Dem*_*ist 6

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

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

       /* getter and setter */
   }
Run Code Online (Sandbox Code Playgroud)
  • 首先,为您的 dto 编写一个超类,例如 BusDto。
  • 其次,更改你的dto以扩展超类,并更改dto的set方法,将属性名称和值放在changedAttrs中(因为无论是否为null,当属性有值时spring都会调用set)。
  • 第三,遍历地图。


Zaa*_*hid 5

另一个选择是使用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 ... */
}
Run Code Online (Sandbox Code Playgroud)

如果未设置firstName,则该值为null,@ JsonInclude批注将忽略该值。否则,如果在请求对象中隐式设置,则firstName将不为null,而firstName.get()将为null。我在此浏览了@laffuste的解决方案,该解决方案在另一条注释中链接到较低的位置(garretwilson的最初评论说它不起作用,结果证明行得通)。

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

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 ...
    }
}
Run Code Online (Sandbox Code Playgroud)

使用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 ... */
}
Run Code Online (Sandbox Code Playgroud)

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

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

还可能值得一提的是,对Optional的使用似乎受到了激烈的争论,而在撰写本文之时,Lombok的维护者将不支持它(例如,参见此问题)。这意味着在具有带有约束的Optional字段的类上使用lombok.Data/lombok.Setter无效(尝试创建具有完整约束的setter),因此使用@ Setter / @ Data会引发异常,因为setter和member变量已设置约束。写不带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...
}
Run Code Online (Sandbox Code Playgroud)