25

我们正在尝试在 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,填充iddisplayName。将其与数据库中的相应字段CustomObjectMerger合并,更新.updateUserdbUserupdateUser

问题是 SpringupdateUser使用默认值和其他自动生成的字段值填充了一些字段,这些字段在合并时会覆盖我们在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几乎完全提供了我正在寻找的东西,减去缺少的 JSON 功能时,即使必须首先使用 a 也会让人感觉很糟糕。如果有人知道如何让 Spring 做到这一点,我将不胜感激!

4

8 回答 8

25

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

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

希望这可以帮助某人,

编辑:

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

于 2013-02-28T20:56:32.067 回答
4

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

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

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

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

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

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

于 2013-02-28T06:35:21.537 回答
3

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

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

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

MergerProcessor.merge(pojoUi, pojoDb);

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

混帐:https ://github.com/nfrpaiva/ui-merge

于 2015-11-28T05:05:44.147 回答
3

可以使用以下方法。

对于这种情况,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);
    ...
}

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

于 2016-09-03T22:07:57.063 回答
2

我有一个使用 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
}

来自请求的字段中的“空”值意味着“不要更改它!”。

名称以“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());
于 2019-04-30T10:17:32.053 回答
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对象映射机制。有时它只是混淆了很多。

于 2013-02-28T04:29:31.920 回答
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);
    }
于 2019-05-30T20:34:04.683 回答
0

部分更新可以通过使用@SessionAttributes功能来解决,这些功能可以执行您自己使用customObjectMerger.

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

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

于 2015-09-30T07:12:54.740 回答