【问题标题】:Spring REST partial update with @PATCH method使用 @PATCH 方法的 Spring REST 部分更新
【发布时间】:2017-12-25 06:58:52
【问题描述】:

我正在尝试根据以下内容对 Manager 实体进行部分更新:

实体

public class Manager {
    private int id;
    private String firstname;
    private String lastname;
    private String username;
    private String password;

    // getters and setters omitted
}

Controller 中的 SaveManager 方法

@RequestMapping(value = "/save", method = RequestMethod.PATCH)
public @ResponseBody void saveManager(@RequestBody Manager manager){
    managerService.saveManager(manager);
}

在 Dao impl 中保存对象管理器。

@Override
public void saveManager(Manager manager) {  
    sessionFactory.getCurrentSession().saveOrUpdate(manager);
}

当我保存对象时,用户名和密码已正确更改,但其他值为空。

所以我需要做的是更新用户名和密码并保留所有剩余数据。

【问题讨论】:

  • 我在post 中汇总了有关如何使用PATCH 的一些细节。这篇文章中描述的方法在 GitHub 上的 example 中使用。

标签: java spring rest


【解决方案1】:

如果你真的在使用 PATCH,那么你应该使用 RequestMethod.PATCH,而不是 RequestMethod.POST。

您的补丁映射应该包含您可以用来检索要修补的 Manager 对象的 id。此外,它应该只包括您要更改的字段。在您的示例中,您发送的是整个实体,因此您无法辨别实际更改的字段(空是否意味着不理会该字段或实际上将其值更改为空)。

也许这样的实现就是您所追求的?

@RequestMapping(value = "/manager/{id}", method = RequestMethod.PATCH)
public @ResponseBody void saveManager(@PathVariable Long id, @RequestBody Map<Object, Object> fields) {
    Manager manager = someServiceToLoadManager(id);
    // Map key is field name, v is value
    fields.forEach((k, v) -> {
       // use reflection to get field k on manager and set it to value v
        Field field = ReflectionUtils.findField(Manager.class, k);
        field.setAccessible(true);
        ReflectionUtils.setField(field, manager, v);
    });
    managerService.saveManager(manager);
}

【讨论】:

  • RequestBody是纯文本形式的情况下怎么处理?您是否使用转换器将纯文本转换为地图?
  • @lane.maxwell 是的,如果我们使用 RequestMethod.PATCH,我完全同意您的回答,那么我们需要使用 Map,但我有一些困惑,请您清除它吗?,如果我将方法类型 PATCH 更改为PUT 并保留所有更改,因为它对数据库预期的工作方式相同,然后它有什么不同。我知道 PUT 和 PATCH 的两个区别 - 1.如果不存在,则创建新条目。 2. 仅更改特定值而不是整个 JSON。对于 JPA 有用吗?是的,然后怎么做?
  • @DhwanilPatel 从您的代码及其行为的角度来看,PUT 和 PATCH 甚至 POST 的行为都将相同。为了遵守 HTTP 规范,PUT 请求应该包含整个实体,而 PATCH 将只包含您要修改的属性。
  • 感谢您的解决方案。但是当我没有写命令时我有 InvocationTargetException:“field.setAccessible(true);”在“findField”和“setField”之间。希望它可以帮助某人。
  • 这是一个很好的评论,因为 setField 假设这些字段是可访问的。我要修改答案
【解决方案2】:

有了这个,您可以修补您的更改

1. Autowire `ObjectMapper` in controller;

2. @PatchMapping("/manager/{id}")
    ResponseEntity<?> saveManager(@RequestBody Map<String, String> manager) {
        Manager toBePatchedManager = objectMapper.convertValue(manager, Manager.class);
        managerService.patch(toBePatchedManager);
    }

3. Create new method `patch` in `ManagerService`

4. Autowire `NullAwareBeanUtilsBean` in `ManagerService`

5. public void patch(Manager toBePatched) {
        Optional<Manager> optionalManager = managerRepository.findOne(toBePatched.getId());
        if (optionalManager.isPresent()) {
            Manager fromDb = optionalManager.get();
            // bean utils will copy non null values from toBePatched to fromDb manager.
            beanUtils.copyProperties(fromDb, toBePatched);
            updateManager(fromDb);
        }
    }

您必须扩展 BeanUtilsBean 以实现复制非空值行为。

public class NullAwareBeanUtilsBean extends BeanUtilsBean {

    @Override
    public void copyProperty(Object dest, String name, Object value)
            throws IllegalAccessException, InvocationTargetException {
        if (value == null)
            return;
        super.copyProperty(dest, name, value);
    }
}

最后,将 NullAwareBeanUtilsBean 标记为 @Component

NullAwareBeanUtilsBean注册为bean

@Bean
public NullAwareBeanUtilsBean nullAwareBeanUtilsBean() {
    return new NullAwareBeanUtilsBean();
}

【讨论】:

  • “用户”从何而来,又是什么?
  • @Boris 我的错,它应该是 manager 参数。更正了代码。
  • 如果我们真的想将一个字段值更改为 NULL 怎么办!
  • 从 BeanAwareNullUtils 中移除“value ==null”检查
  • 好答案。而且更容易,因为BeanUtilsBean 的文档指出:如果源“bean”实际上是一个 Map,则假定它包含字符串值的简单属性名称作为键,指向相应的属性值将被转换(如有必要)并在目标 bean 中设置。 也就是说,Jackson 部分和 NullAwareBeanUtilsBean 部分也可以跳过。只需使用BeanUtilsBean.getInstance().copyProperties(fromDb, manager);
【解决方案3】:

首先,您需要知道您是在进行插入还是更新。插入很简单。更新时,使用 get() 检索实体。然后更新任何字段。在事务结束时,Hibernate 将刷新更改并提交。

【讨论】:

    【解决方案4】:

    您可以编写仅更新特定字段的自定义更新查询:

    @Override
    public void saveManager(Manager manager) {  
        Query query = sessionFactory.getCurrentSession().createQuery("update Manager set username = :username, password = :password where id = :id");
        query.setParameter("username", manager.getUsername());
        query.setParameter("password", manager.getPassword());
        query.setParameter("id", manager.getId());
        query.executeUpdate();
    }
    

    【讨论】:

    • 效果不错,但是这种情况下用POST和PATCH是没有关系的吧?
    • 这取决于您的需求。在我看来,最好使用 PUT(或 POST)进行修改
    【解决方案5】:

    ObjectMapper.updateValue 提供了将实体部分映射到 dto 中的值所需的一切。 另外,您可以在此处使用两个:Map&lt;String, Object&gt; fieldsString json,因此您的服务方法可能如下所示:

    @Autowired
    private ObjectMapper objectMapper;
    
    @Override
    @Transactional
    public Foo save(long id, Map<String, Object> fields) throws JsonMappingException {
        Foo foo = fooRepository.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("Foo not found for this id: " + id));
    
        return objectMapper.updateValue(foo , fields);
    }
    

    作为第二个解决方案和对 Lane Maxwell 答案的补充,您可以使用 Reflection 仅映射已发送的值映射中存在的属性,因此您的服务方法可能如下所示:

    @Override
    @Transactional
    public Foo save(long id, Map<String, Object> fields) {
    
        Foo foo = fooRepository.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("Foo not found for this id: " + id));
    
        fields.keySet()
                .forEach(k -> {
                    Method method = ReflectionUtils.findMethod(LocationProduct.class, "set" + StringUtils.capitalize(k));
                    if (method != null) {
                        ReflectionUtils.invokeMethod(method, foo, fields.get(k));
                    }
                });
        return foo;
    }
    

    第二种解决方案允许您在映射过程中插入一些额外的业务逻辑,可能是转换或计算等。

    与通过名称查找反射字段 Field field = ReflectionUtils.findField(Foo.class, k); 以及使其可访问不同,查找属性的设置器实际上调用了可能包含要执行的附加逻辑的设置器方法,并防止将值设置为私有属性。

    【讨论】:

      猜你喜欢
      • 2013-07-25
      • 2018-07-27
      • 2015-07-11
      • 2017-04-14
      • 2021-01-24
      • 1970-01-01
      • 1970-01-01
      • 2015-06-15
      • 1970-01-01
      相关资源
      最近更新 更多