我们正在尝试在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
,填写了id
和displayName
。 CustomObjectMerger
将此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做到这一点,我会非常感激!
答案 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);
它适用于本机属性和集合。
答案 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
所做的事情。
请在此处查看我的答案,尤其是编辑,以帮助您入门:
答案 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);
}