如何在 Spring Web + JPA 中部分更新实体及其关系

How to partially update an entity and its relationships in Spring Web + JPA

我有这两个简单的实体:

@Entity
public class User {
    @Id
    @GeneratedValue
    private Long Id;

    private String username;

    private String firstName;

    @ManyToOne
    private Role role;        

    //Getters and setters...
}

@Entity
public class Role {
    @Id
    @GeneratedValue
    private Long Id;

    private String name;

    @OneToMany(mappedBy="role")
    Set<User> users = new HashSet<>();

    //Getters and setters...
}

在我的控制器中,我需要使用以下 HTTP 请求更新用户实体

curl -X PATCH \
  -H "Content-Type: application/json" \
  -d \
  '{
      "username": null,
      "role": 1
  }' http://localhost:8080/users/1

如您所见,我希望一个字段可以设置为空请求中缺少的一个字段不应更新 并且 可以使用相关模型 ID 设置关系。

这是我在控制器中实现的尝试

@RestController
@RequestMapping("/users")
public class UserRestController {

    private final UserRepository userRepository;
    private final RoleRepository roleRepository;

    @Autowired
    UserRestController (UserRepository userRepository, RoleRepository roleRepository) {
        this.userRepository = userRepository;
        this.roleRepository = roleRepository;
    }

    @RequestMapping(value = "/{userId}", method = {RequestMethod.PATCH, RequestMethod.PUT})
    ResponseEntity<?> updateUser(@PathVariable Long userId, HttpServletRequest request) {

        User userToUpdate = userRepository.findOne(userId);

        ObjectMapper mapper = new ObjectMapper();
        User updatedUser = mapper.readerForUpdating(userToUpdate ).readValue(request.getReader());
        User user = userRepository.save(updatedUser);

        return new ResponseEntity<>(user, new HttpHeaders(), HttpStatus.CREATED);
    }
}

我唯一设法实现的是部分更新和字段为空更新。不过,这样一来,我就无法仅通过 ID 更新关系。 有人可以帮忙吗?

更新

感谢@Naros 更新post,我重写了整个控制器来处理 Role id 更新

@RestController
@RequestMapping("/users")
public class UserRestController {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private RoleRepository roleRepository;

    @Autowired
    private UserService userService;

    @RequestMapping(value = "/{userId}", method = {RequestMethod.PATCH, RequestMethod.PUT})
    ResponseEntity<?> updateUser(@PathVariable Long userId, @RequestBody ObjectNode requestJsonNode) throws IOException {

        User userToUpdate = userRepository.findOne(userId);

        RestMerger restMerger = new RestMerger();
        restMerger.getAssociations().put("role", userService);

        User user = (User) restMerger.merge(userToUpdate, requestJsonNode);
        userRepository.save(user);

        return new ResponseEntity<>(user, new HttpHeaders(), HttpStatus.CREATED);
    }

}

我生成了一个 UserService class 来处理 id-entity 合并功能。

@Service
public class UserService implements AssociationResolver {
    public static final String ROLE_FIELD = "role";

    @Autowired
    private RoleRepository roleRepository;    

    @Override
    public void resolveAssociation(String name, JsonNode objectNode, ObjectNode mainNode) throws JsonProcessingException {
        if(name.equals(ROLE_FIELD))
            resolveRoleAssociation(name, objectNode.get(name).asLong(), mainNode);
    }

    private void resolveRoleAssociation(String name, Long id, ObjectNode mainNode) {
        Role role = roleRepository.findOne(id);
        ObjectMapper mapper = new ObjectMapper();
        mainNode.replace(name, mapper.convertValue(role, JsonNode.class));
    }
}

此服务可能包括完整的控制器 updateUser 功能和字段验证。 RestMerger class 像 ObjectMapper 一样更新实体。

public class RestMerger {
    private HashMap<String, AssociationResolver> associations = new HashMap<>();

    public Object merge(Object mainObject, ObjectNode updateNode) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        ObjectNode mainNode = mapper.convertValue(mainObject, ObjectNode.class);

        Iterator<String> fieldNames = updateNode.fieldNames();
        while (fieldNames.hasNext()) {

            String fieldName = fieldNames.next();
            JsonNode jsonNode = mainNode.get(fieldName);

            if(associations.keySet().contains(fieldName)) {
                associations.get(fieldName).resolveAssociation(fieldName, updateNode, mainNode);
            } else if (jsonNode.isObject())
                merge(jsonNode, updateNode);
            else {
                JsonNode value = updateNode.get(fieldName);
                mainNode.replace(fieldName, value);
            }
        }

        return mapper.treeToValue(mainNode, mainObject.getClass());
    }

    public HashMap<String, AssociationResolver> getAssociations() {
        return associations;
    }
}

最后,AssociationResolver 界面使通用 RestMerger class 与自定义实体服务交互

public interface AssociationResolver {
    void resolveAssociation(String name, JsonNode value, ObjectNode mainNode) throws JsonProcessingException;
}

如果您愿意不使用 ObjectMapper,您可以使用声明性更改重新编码控制器。我看到 ObjectMapper 的好处是,您可以精确控制 RESTful 接口可以修改哪些字段,以及哪些字段(如果提供)可以静默忽略或 warnings/errors 发出。

注意:以下代码片段假定一个名为 UserAttributes 的值对象,它基本上公开了控制器将接受和操作的有效值,而不是输入是动态的基于您的原始代码。

@RestController
@RequestMapping("/users")
public class UserRestController {

  /* other cruff */

  @RequestMapping(
    value = "/{userId}", 
    method = {
      RequestMethod.PATCH, 
      RequestMethod.PUT
  })
  public ResponseEntity<?> updateUser(
    @PathVariable Long userId, 
    @RequestBody UserAttributes userAttributes) {

    User userToUpdate = userRepository.findOne(userId);
    if(userToUpdate == null) {
      /* handle cannot find user by userid */
    }

    // update the username from the user object
    userToUpdate.setUsername(userAttributes.getUserName());

    if(userAttributes.getRole() != null) {
      Role roleToUpdate = roleRepository.findOne(userAttributes.getRole());
      if(roleToUpdate == null) {
        /* handle cannot find the role */
      }
      roleToUpdate.getUsers().add(userToUpdate);
      userToUpdate.setRole(roleToUpdate);
      roleRepository.save(roleToUpdate);
    }
    else {
      userToUpdate.setRole(null);
    }

    userToUpdate = userRepository.save(userToUpdate);

    return new ResponseEntity<>(
      userToUpdate,
      new HttpHeaders(),
      HttpStatus.CREATED);
  }
}

更新

可能有帮助的特殊 JsonNode 合并函数:

public static JsonNode merge(JsonNode mainNode, JsonNode updateNode) {

   Iterator<String> fieldNames = updateNode.fieldNames();
    while (fieldNames.hasNext()) {

        String fieldName = fieldNames.next();
        JsonNode jsonNode = mainNode.get(fieldName);
        // if field exists and is an embedded object
        if (jsonNode != null && jsonNode.isObject()) {
            merge(jsonNode, updateNode.get(fieldName));
        }
        else {
            if (mainNode instanceof ObjectNode) {
                // Overwrite field
                JsonNode value = updateNode.get(fieldName);
                ((ObjectNode) mainNode).put(fieldName, value);
            }
        }

    }

    return mainNode;
}