Spring部分更新对象数据绑定

时间:2013-02-27 23:49:51

标签: java spring spring-mvc

我们正在尝试在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的字段。但是,即使不得不首先使用customObjectMerger(),当spring提供几乎正是我正在寻找的内容时,也会感到hacky,减去缺少的JSON功能。如果有人知道如何让Spring做到这一点,我会非常感激!

8 个答案:

答案 0 :(得分:21)

我刚遇到同样的问题。我目前的解决方案是这样的。我还没有做太多测试,但是在初步检查时看起来效果还不错。

@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 :(得分:1)

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

  • 创建一个使用@ modelattribute注释的方法,该方法根据存储库中的路径变量加载用户。

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

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

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

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

答案 2 :(得分:1)

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

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

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

MergerProcessor.merge(pojoUi, pojoDb);

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

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

答案 3 :(得分: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);
    ...
}

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

答案 4 :(得分: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);
    ...
}

在上述功能中,您可以调用一些私人功能&amp; classes(userRepository,customObjectMerger,...),但不解释它是如何工作的或这些函数的外观。所以我只能猜测:

  

CustomObjectMerger将此updateUser与相应的更新   来自数据库的dbUser,更新包含的唯一字段   UpdateUser两个。

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

您的案例中有两个选项:

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

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

如果您认为上述两种解决方案并不满意,请考虑自己解析json请求,而不必担心Spring对象映射机制。有时它只是混淆了很多。

答案 5 :(得分:0)

使用@SessionAttributes功能可以解决部分更新问题,这些功能是为了完成您使用customObjectMerger所做的事情。

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

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

答案 6 :(得分:0)

我有一个定制的肮脏解决方案,它使用java.lang.reflect包。我的解决方案在3年内都运行良好,没有问题。

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

代码只是这样做:

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

答案 7 :(得分: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);
    }