Подтвердить что ты не робот

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, чтобы это сделать, я был бы очень признателен!

4b9b3361

Ответ 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());