Spring部分更新对象数据绑定

问题描述 投票:23回答:8

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

我们对所有字段进行内联编辑,因此当用户编辑字段并确认时,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的字段。但是,即使不得不首先使用customObjectMerger(),当春天提供几乎正是我正在寻找的东西时,也会感到很难过,减去缺少的JSON功能。如果有人知道如何让Spring做到这一点,我将非常感激!

java spring spring-mvc
8个回答
22
投票

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

@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。

希望这有助于某人,

编辑:

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


1
投票

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

  • 创建一个使用@ modelattribute注释的方法,该方法基于存储库的路径变量加载用户。
  • 使用参数@modelattribute创建一个方法@Requestmapping

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

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

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


1
投票

我构建了一个API,在调用persist或merge或update之前将视图对象与实体合并。

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

只需在POJO`S字段中使用注释UIAttribute,然后使用:

MergerProcessor.merge(pojoUi, pojoDb);

它适用于本机属性和集合。

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


1
投票

可以使用以下方法。

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

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

将该String转换为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);
    ...
}

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


0
投票

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

@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合并,更新updateUser中包含的唯一字段。

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

您的案例有两种选择:

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

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

如果您觉得上述2个解决方案不满意,请考虑自己解析json请求,而不要打扰Spring对象映射机制。有时它只是混淆了很多。


0
投票

部分更新可以通过使用@SessionAttributes功能来解决,这些功能可以用customObjectMerger做你自己做的事情。

看看我的答案,特别是编辑,让你开始:

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


0
投票

我有一个定制的脏解决方案,使用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值表示“将其设置为null”。

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

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

0
投票

我用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);
    }
© www.soinside.com 2019 - 2024. All rights reserved.