【问题标题】:Spring Partial Update Object Data BindingSpring 部分更新对象数据绑定
【发布时间】:2013-02-14 00:19:44
【问题描述】:

我们正在尝试在 Spring 3.2 中实现一个特殊的部分更新功能。我们使用 Spring 作为后端,并有一个简单的 Javascript 前端。我一直无法找到满足我们要求的直接解决方案,即 update() 函数应该接受任意数量的 field:values 并相应地更新持久性模型。

我们对所有字段进行内联编辑,因此当用户编辑字段并确认时,id 和修改后的字段会以 json 格式传递给控制器​​。控制器应该能够从客户端接收任意数量的字段(1 到 n)并仅更新这些字段。

例如,当 id==1 的用户编辑他的 displayName 时,发布到服务器的数据如下所示:

{"id":"1", "displayName":"jim"}

目前,我们在 UserController 中有一个不完整的解决方案,如下所述:

@RequestMapping(value = "/{id}", method = RequestMethod.POST )
public @ResponseBody ResponseEntity<User> update(@RequestBody User updateUser) {
    dbUser = userRepository.findOne(updateUser.getId());
    customObjectMerger(updateUser, dbUser);
    userRepository.saveAndFlush(updateUuser);
    ...
}

这里的代码有效,但有一些问题:@RequestBody 创建一个新的updateUser,填写iddisplayNameCustomObjectMerger 将此 updateUser 与数据库中对应的 dbUser 合并,更新 updateUser 中包含的唯一字段。

问题在于 Spring 使用默认值和其他自动生成的字段值填充 updateUser 中的某些字段,这些字段在合并时会覆盖 dbUser 中的有效数据。明确声明它应该忽略这些字段不是一种选择,因为我们希望我们的 update 也能够设置这些字段。

我正在寻找某种方法让 Spring 仅将显式发送到 update() 函数的信息自动合并到 dbUser 中(不重置默认/自动字段值)。有什么简单的方法吗?

更新:我已经考虑过以下选项,它几乎可以满足我的要求,但并不完全。问题是它以@RequestParam 的形式获取更新数据,并且(AFAIK)不执行 JSON 字符串:

//load the existing user into the model for injecting into the update function
@ModelAttribute("user")
public User addUser(@RequestParam(required=false) Long id){
    if (id != null) return userRepository.findOne(id);
    return null;
}
....
//method declaration for using @MethodAttribute to pre-populate the template object
@RequestMapping(value = "/{id}", method = RequestMethod.POST )
public @ResponseBody ResponseEntity<User> update(@ModelAttribute("user") User updateUser){
....
}

我已经考虑重写我的customObjectMerger() 以更适合使用 JSON,计算并让它仅考虑来自HttpServletRequest 的字段。但是,当 spring 提供 几乎正是我正在寻找的东西,减去缺少的 JSON 功能时,即使必须首先使用 customObjectMerger() 也会让人觉得很麻烦。如果有人知道如何让 Spring 做到这一点,我将不胜感激!

【问题讨论】:

  • @SamEsla - 你有没有找到更好的解决方案,特别是在有嵌套对象的情况下?我有同样的问题,在阅读这篇文章之前,做了一些和你类似的事情。如果你有时间请看:stackoverflow.com/questions/16473727/…

标签: java spring spring-mvc


【解决方案1】:

我刚刚遇到了同样的问题。我目前的解决方案看起来像这样。我还没有做太多的测试,但在初步检查时,它看起来工作得很好。

@Autowired ObjectMapper objectMapper;
@Autowired UserRepository userRepository;

@RequestMapping(value = "/{id}", method = RequestMethod.POST )
public @ResponseBody ResponseEntity<User> update(@PathVariable Long id, HttpServletRequest request) throws IOException
{
    User user = userRepository.findOne(id);
    User updatedUser = objectMapper.readerForUpdating(user).readValue(request.getReader());
    userRepository.saveAndFlush(updatedUser);
    return new ResponseEntity<>(updatedUser, HttpStatus.ACCEPTED);
}

ObjectMapper 是一个 org.codehaus.jackson.map.ObjectMapper 类型的 bean。

希望这对某人有所帮助,

编辑:

遇到了子对象的问题。如果子对象接收到要部分更新的属性,它将创建一个新对象,更新该属性并设置它。这会删除该对象上的所有其他属性。如果我遇到一个干净的解决方案,我会更新。

【讨论】:

  • 谢谢 Tyler,这似乎是一个方便的解决方案。我仍然想知道是否有一个 Spring 函数允许将 JSON 从 POST 数据直接/自动绑定到持久对象。
  • 感谢您的解决方案。我认为解决这种情况的唯一方法是手动处理。以前从未认识过 Jackson ObjectMapper...
  • @Tyler - 你有没有找到更好的解决方案,特别是在有嵌套对象的情况下?我有同样的问题,在阅读这篇文章之前,做了一些类似于 OP 的事情。有时间请看:stackoverflow.com/questions/16473727/…
  • @user36123 - 看看 Jackson 的这个老错误:jira.codehaus.org/browse/JACKSON-679 有一条评论显示了一些可以进行深度更新的代码。当然,API 自 2011 年以来发生了变化,但我们能够对其进行修复并使其正常工作。
  • @MrException - 感谢您指出这一点。该项目现已完成并尘埃落定。但是您提供的详细信息,没有实际测试代码 sn-p,正是我想要的。希望它可以帮助阅读这篇文章寻找相同解决方案的其他人。如果您能够修复它以使用新的 api - 如果您能用您的解决方案更新我的 POST (stackoverflow.com/questions/16473727/…) 将不胜感激。如果它有效,我会将您的贡献视为正确答案!谢谢。
【解决方案2】:

我们正在使用@ModelAttribute 来实现您想要做的事情。

  • 创建一个使用@model 属性注释的方法,该方法通过存储库基于路径变量加载用户。

  • 使用参数@modelattribute 创建一个方法@Requestmapping

这里的重点是@modelattribute 方法是模型的初始化器。然后 spring 将请求与这个模型合并,因为我们在 @requestmapping 方法中声明了它。

这为您提供了部分更新功能。

一些,甚至很多? ;) 会争辩说这是不好的做法,因为我们直接在控制器中使用我们的 DAO,而不是在专用服务层中进行合并。但是目前我们并没有因为这种方法而遇到问题。

【讨论】:

  • 谢谢马丁,但我已经尝试过了。这种方法的问题是它似乎只接受@RequestParam 来更新字段。我们想要一种方法来复制您提到的这个确切功能,但使用 JSON 作为输入。我确信 Spring 在某个地方为此提供了内置功能,但我还没有遇到过。
  • 这应该类似。确实,我们目前将它用于发布数据,但我将尝试使用 json 对象来实现。最后弹簧处理这个非常相似。应该合并与您的模型 arributes 匹配的任何内容(requestparam、json props)。明天我会尝试做一个简短的例子,因为我在 ipad atm 上。
  • 我的答案迟到了 3 周以上。但最后我有时间稍微检查一下。黄龙是对的。没有更简洁的方式来处理 json 请求体。看起来我们缺少一个 requestbody 合并 :) 可能是一个 spring 请求?
  • @Martin - 你有没有向 Spring 社区提出这个要求?我现在有一个类似的问题。我希望能够使用 JSON 请求类型发出 post 请求,并使用由于 POST 请求而更改的值更新现有的 Command 对象。如果有兴趣,或者有时间,请看stackoverflow.com/questions/16473727/…
  • 好的,我已经为此打开了一个 JIRA:jira.springsource.org/browse/SPR-10552
【解决方案3】:

我构建了一个 API,在调用持久化或合并或更新之前将视图对象与实体合并。

这是第一个版本,但我认为这是一个开始。

只需在你的 POJO`S 字段中使用注解 UIAttribute 然后使用:

MergerProcessor.merge(pojoUi, pojoDb);

它适用于原生属性和集合。

git:https://github.com/nfrpaiva/ui-merge

【讨论】:

    【解决方案4】:

    可以使用以下方法。

    对于这种情况,PATCH 方法会更合适,因为实体将被部分更新。

    在控制器方法中,将请求体作为字符串。

    将该字符串转换为 JSONObject。然后遍历键并使用传入数据更新匹配变量。

    import org.json.JSONObject;
    
    @RequestMapping(value = "/{id}", method = RequestMethod.PATCH )
    public ResponseEntity<?> updateUserPartially(@RequestBody String rawJson, @PathVariable long id){
    
        dbUser = userRepository.findOne(id);
    
        JSONObject json = new JSONObject(rawJson);
    
        Iterator<String> it = json.keySet().iterator();
        while(it.hasNext()){
            String key = it.next();
            switch(key){
                case "displayName":
                    dbUser.setDisplayName(json.get(key));
                    break;
                case "....":
                    ....
            }
        }
        userRepository.save(dbUser);
        ...
    }
    

    这种方法的缺点是,您必须手动验证传入的值。

    【讨论】:

    • 您好!感谢您的回复。是的,事后看来 PATCH 似乎是自然的解决方案,但我想我们都没有考虑过它,因为它是在几个月前作为 3.2 版中的新 Spring Framework 功能实现的。
    【解决方案5】:

    我有一个使用 java.lang.reflect 包的定制和肮脏的解决方案。我的解决方案运行了 3 年没有问题。

    我的方法有 2 个参数,objectFromRequest 和 objectFromDatabase 都具有 Object 类型。

    代码就是这样:

    if(objectFromRequest.getMyValue() == null){
       objectFromDatabase.setMyValue(objectFromDatabase.getMyValue); //change nothing
    } else {
       objectFromDatabase.setMyValue(objectFromRequest.getMyValue); //set the new value
    }
    

    请求字段中的“null”值表示“不要更改它!”。

    名称以“Id”结尾的引用列的-1 值表示“将其设置为空”。

    您还可以为您的不同场景添加许多自定义修改。

    public static void partialUpdateFields(Object objectFromRequest, Object objectFromDatabase) {
        try {
            Method[] methods = objectFromRequest.getClass().getDeclaredMethods();
    
            for (Method method : methods) {
                Object newValue = null;
                Object oldValue = null;
                Method setter = null;
                Class valueClass = null;
                String methodName = method.getName();
                if (methodName.startsWith("get") || methodName.startsWith("is")) {
                    newValue = method.invoke(objectFromRequest, null);
                    oldValue = method.invoke(objectFromDatabase, null);
    
                    if (newValue != null) {
                        valueClass = newValue.getClass();
                    } else if (oldValue != null) {
                        valueClass = oldValue.getClass();
                    } else {
                        continue;
                    }
                    if (valueClass == Timestamp.class) {
                        valueClass = Date.class;
                    }
    
                    if (methodName.startsWith("get")) {
                        setter = objectFromRequest.getClass().getDeclaredMethod(methodName.replace("get", "set"),
                                valueClass);
                    } else {
                        setter = objectFromRequest.getClass().getDeclaredMethod(methodName.replace("is", "set"),
                                valueClass);
                    }
    
                    if (newValue == null) {
                        newValue = oldValue;
                    }
    
                    if (methodName.endsWith("Id")
                            && (valueClass == Number.class || valueClass == Integer.class || valueClass == Long.class)
                            && newValue.equals(-1)) {
                        setter.invoke(objectFromDatabase, new Object[] { null });
                    } else if (methodName.endsWith("Date") && valueClass == Date.class
                            && ((Date) newValue).getTime() == 0l) {
                        setter.invoke(objectFromDatabase, new Object[] { null });
                    } 
                    else {
                        setter.invoke(objectFromDatabase, newValue);
                    }
                }
    
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    

    在我的 DAO 类中,simcardToUpdate 来自 http 请求:

    simcardUpdated = (Simcard) session.get(Simcard.class, simcardToUpdate.getId());
    
    MyUtil.partialUpdateFields(simcardToUpdate, simcardUpdated);
    
    updatedEntities = Integer.parseInt(session.save(simcardUpdated).toString());
    

    【讨论】:

      【解决方案6】:

      主要问题在于您的以下代码:

      @RequestMapping(value = "/{id}", method = RequestMethod.POST )
      public @ResponseBody ResponseEntity<User> update(@RequestBody User updateUser) {
          dbUser = userRepository.findOne(updateUser.getId());
          customObjectMerger(updateUser, dbUser);
          userRepository.saveAndFlush(updateUuser);
          ...
      }
      

      在上述函数中,您调用了一些私有函数和类(userRepository、customObjectMerger、...),但没有说明它是如何工作的或这些函数的外观。所以我只能猜测:

      CustomObjectMerger 将此 updateUser 与相应的 dbUser 来自数据库,更新仅包含在 更新用户。

      这里我们不知道 CustomObjectMerger 中发生了什么(这是你的函数,你没有显示它)。但是根据您的描述,我可以猜测:您将所有属性从 updateUser 复制到数据库中的对象。这绝对是错误的方式,因为Spring映射对象时,它会填充所有数据。而且您只想更新一些特定的属性。

      您的情况有两种选择:

      1) 将所有属性(包括未更改的属性)发送到服务器。这可能会花费更多带宽,但您仍然可以按自己的方式行事

      2) 您应该设置一些特殊值作为 User 对象的默认值(例如,id = -1,age = -1...)。然后在 customObjectMerger 中设置不是 -1 的值。

      如果你觉得上面2个方案都不满意,可以考虑自己解析json请求,不要纠结Spring对象映射机制。有时它只是让很多人感到困惑。

      【讨论】:

      • 您好黄,谢谢您的回答。 @RequestBody 将我的有效负载转换为 User 对象的新实例。私有方法的内部对于手头的问题并不重要。请查看 Martin Fey 的回答和我的更新,以了解 @ModelAttribute 如何提供我正在寻找的相同功能,但与请求中的 RequestParams 一起使用。使用这种方法,Spring 自动将来自RequestParams 的新信息绑定到它从数据库中获取的现有对象。我一直在搜索 Spring Docs,寻找一种使用 JSON 有效负载执行此操作的方法。
      • @SamEsla:我理解 Fey 的方法和你的问题。在我看来,Fey 的方式很聪明,但这并不是一个很好的做法。我自己在之前使用 ModelAttribute 时遇到了一些问题,特别是,正如他评论的那样,那是在控制器中使用 DAO 对象。
      • @SamEsla:哦,我看错了你的问题。我写了RequestBody,但不知何故我认为你的意思是ResponseBody。我的眼睛有多糟糕@_@我已经更新了答案以删除不相关的部分。
      • 感谢 Hoang,是的,@RequestBody 使用 ObjectMapper 将来自请求的传入数据绑定到指定对象的新实例。
      【解决方案7】:

      部分更新可以通过使用@SessionAttributes 功能来解决,该功能可以完成您使用customObjectMerger 所做的事情。

      在此处查看我的答案,尤其是编辑内容,以帮助您入门:

      https://stackoverflow.com/a/14702971/272180

      【讨论】:

        【解决方案8】:

        我用 java Map 和一些反射魔法完成了这个:

        public static Entidade setFieldsByMap(Map<String, Object> dados, Entidade entidade) {
                dados.entrySet().stream().
                        filter(e -> e.getValue() != null).
                        forEach(e -> {
                            try {
                                Method setter = entidade.getClass().
                                        getMethod("set"+ Strings.capitalize(e.getKey()),
                                                Class.forName(e.getValue().getClass().getTypeName()));
                                setter.invoke(entidade, e.getValue());
                            } catch (Exception ex) { // a lot of exceptions
                                throw new WebServiceRuntimeException("ws.reflection.error", ex);
                            }
                        });
                return entidade;
            }
        

        以及入口点:

            @Transactional
            @PatchMapping("/{id}")
            public ResponseEntity<EntityOutput> partialUpdate(@PathVariable String entity,
                    @PathVariable Long id, @RequestBody Map<String, Object> data) {
                // ...
                return new ResponseEntity<>(obj, HttpStatus.OK);
            }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多