Spring Частичное обновление привязки данных объекта
Мы пытаемся реализовать специальную функцию частичного обновления в Spring 3.2. Мы используем Spring для бэкэнд и имеем простой интерфейс Javascript. Я не смог найти прямое решение для наших требований. Функция update() должна принимать любое количество полей: значения и соответственно обновлять модель сохранения.
У нас есть встроенное редактирование для всех наших полей, так что, когда пользователь редактирует поле и подтверждает, идентификатор и измененное поле передаются контроллеру как 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 обеспечивает практически то, что я ищу, минус отсутствие функций JSON. Если кто-нибудь знает, как получить Spring, чтобы это сделать, я был бы очень признателен!
Ответы
Ответ 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 - это bean типа org.codehaus.jackson.map.ObjectMapper.
Надеюсь, это поможет кому-то,
Edit:
Были проблемы с дочерними объектами. Если дочерний объект получает свойство для частичного обновления, он создаст новый объект, обновит это свойство и установит его. Это стирает все остальные свойства этого объекта. Я обновлю, если найду чистое решение.
Ответ 2
Мы используем @ModelAttribute для достижения того, что вы хотите сделать.
-
Создайте метод, аннотированный с помощью @modelattribute, который загружает пользователя на основе переменной pathvariable throguh в репозитории.
-
создать метод @Requestmapping с параметром @modelattribute
Дело здесь в том, что метод @modelattribute является инициализатором для модели. Затем spring объединяет запрос с этой моделью, так как мы объявляем его в методе @requestmapping.
Это дает вам частичную функциональность обновления.
Некоторые или даже много?;) будет утверждать, что это плохая практика, так как мы используем наши DAO непосредственно в контроллере и не делаем этого слияния в выделенном уровне обслуживания. Но в настоящее время мы не сталкивались с проблемами из-за этого подхода.
Ответ 3
Я создаю API, который объединяет объекты представления с сущностями перед вызовом persiste или слиянием или обновлением.
Это первая версия, но я думаю, что это начало.
Просто используйте аннотацию UIAttribute в полях POJO`S, а затем используйте:
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);
...
}
Нижняя сторона этого подхода заключается в том, что вы должны вручную проверять входящие значения.
Ответ 5
Основная проблема заключается в следующем коде:
@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 отображает объект, он заполняет все данные. И вы хотите обновить некоторые конкретные свойства.
В вашем случае есть 2 варианта:
1) Отправка всех свойств (включая неизменные свойства) на сервер. Это может стоить немного больше полосы пропускания, но вы по-прежнему сохраняете свой путь
2) Вы должны установить некоторые специальные значения в качестве значения по умолчанию для объекта User (например, id = -1, age = -1...). Затем в customObjectMerger вы просто устанавливаете значение, которое не равно -1.
Если вы считаете, что 2 вышеуказанных решения не выполняются, подумайте о том, чтобы проанализировать запрос json самостоятельно и не беспокоиться о механизме отображения объектов Spring. Иногда это просто путают много.
Ответ 6
Частичные обновления могут быть решены с помощью @SessionAttributes
функциональности, которые сделаны для того, чтобы делать то, что вы сделали с помощью customObjectMerger
.
Посмотрите мой ответ здесь, особенно изменения, чтобы вы начали:
fooobar.com/questions/274861/...
Ответ 7
У меня индивидуальное и грязное решение, использующее пакет 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
}
"Нулевое" значение в поле из запроса означает "не меняйте его!".
Значение -1 для ссылочного столбца, имя которого заканчивается на "Id", означает "Установить его на ноль".
Вы также можете добавить множество пользовательских модификаций для разных сценариев.
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());