Как различить нулевые и не предоставленные значения для частичных обновлений в Spring Rest Controller

Я пытаюсь различить нулевые значения и не предоставленные значения при частичном обновлении сущности с помощью метода запроса PUT в Spring Rest Controller.

Рассмотрим в качестве примера следующую сущность:

@Entity
private class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /* let's assume the following attributes may be null */
    private String firstName;
    private String lastName;

    /* getters and setters ... */
}

Мой личный репозиторий (Spring Data):

@Repository
public interface PersonRepository extends CrudRepository<Person, Long> {
}

Я использую DTO:

private class PersonDTO {
    private String firstName;
    private String lastName;

    /* getters and setters ... */
}

Мой Spring RestController:

@RestController
@RequestMapping("/api/people")
public class PersonController {

    @Autowired
    private PersonRepository people;

    @Transactional
    @RequestMapping(path = "/{personId}", method = RequestMethod.PUT)
    public ResponseEntity<?> update(
            @PathVariable String personId,
            @RequestBody PersonDTO dto) {

        // get the entity by ID
        Person p = people.findOne(personId); // we assume it exists

        // update ONLY entity attributes that have been defined
        if(/* dto.getFirstName is defined */)
            p.setFirstName = dto.getFirstName;

        if(/* dto.getLastName is defined */)
            p.setLastName = dto.getLastName;

        return ResponseEntity.ok(p);
    }
}

Запрос с отсутствующим свойством

{"firstName": "John"}

Ожидаемое поведение: обновлениеfirstName= "John" (ПокидатьlastName без изменений).

Запрос с нулевым свойством

{"firstName": "John", "lastName": null}

Ожидаемое поведение: обновлениеfirstName="John" и установитьlastName=null.

Я не могу различить эти два случая, так какlastName в DTO всегда установленnull Джексон.

Примечание: я знаю, что лучшие практики REST (RFC 6902) рекомендуют использовать PATCH вместо PUT для частичных обновлений, но в моем конкретном сценарии мне нужно использовать PUT.

Ответы на вопрос(4)

По умолчанию не сбрасывайте «нулевые» значения. Предоставьте явный список через параметры запроса, какие поля вы хотите сбросить. Таким образом, вы все равно можете отправлять JSON, который соответствует вашей сущности, и иметь гибкость для отмены полей при необходимости.

В зависимости от вашего варианта использования некоторые конечные точки могут явно обрабатывать все нулевые значения как неустановленные операции. Немного опасно для исправления, но в некоторых случаях может быть вариант.

Решение Вопроса

если игнорировать проверку, вы можете решить вашу проблему следующим образом.

   public class BusDto {
       private Map<String, Object> changedAttrs = new HashMap<>();

       /* getter and setter */
   }
Во-первых, напишите суперкласс для вашего dto, например BusDto.Во-вторых, измените dto, чтобы расширить суперкласс, и измените метод set dto, чтобы присвоить имя и значение атрибута для ChangeAttrs (поскольку пружина будет вызывать набор, когда атрибут имеет значение независимо от нуля или не равно нулю).В-третьих, обход карты.

зованииJsonNode как DTO. Таким образом, вы получите только то, что представлено.

Вам нужно будет написатьMergeService Вы сами делаете настоящую работу, похожую на BeanWrapper. Я не нашел существующую структуру, которая может делать именно то, что нужно. (Если вы используете только запросы Json, вы можете использовать JacksonsreadForUpdate Метод.)

На самом деле мы используем другой тип узла, так как нам нужны те же функциональные возможности, что и в «стандартных отправках форм» и других вызовах службы. Кроме того, изменения должны применяться в транзакции внутри чего-то, называемогоEntityService.

этоMergeService к сожалению, станет довольно сложным, так как вам придется самостоятельно обрабатывать свойства, списки, наборы и карты :)

Самым проблемным для меня было провести различие между изменениями в элементе списка / набора и модификациями или заменами списков / наборов.

А также проверка будет нелегкой, так как вам нужно проверить некоторые свойства по другой модели (в моем случае сущности JPA)

РЕДАКТИРОВАТЬ - Некоторый код сопоставления (псевдокод):

class SomeController { 
   @RequestMapping(value = { "/{id}" }, method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public void save(
            @PathVariable("id") final Integer id,
            @RequestBody final JsonNode modifications) {
        modifierService.applyModifications(someEntityLoadedById, modifications);
    }
}

class ModifierService {

    public void applyModifications(Object updateObj, JsonNode node)
            throws Exception {

        BeanWrapperImpl bw = new BeanWrapperImpl(updateObj);
        Iterator<String> fieldNames = node.fieldNames();

        while (fieldNames.hasNext()) {
            String fieldName = fieldNames.next();
            Object valueToBeUpdated = node.get(fieldName);
            Class<?> propertyType = bw.getPropertyType(fieldName);
            if (propertyType == null) {
               if (!ignoreUnkown) {
                    throw new IllegalArgumentException("Unkown field " + fieldName + " on type " + bw.getWrappedClass());
                }
            } else if (Map.class.isAssignableFrom(propertyType)) {
                    handleMap(bw, fieldName, valueToBeUpdated, ModificationType.MODIFY, createdObjects);
            } else if (Collection.class.isAssignableFrom(propertyType)) {
                    handleCollection(bw, fieldName, valueToBeUpdated, ModificationType.MODIFY, createdObjects);
            } else {
                    handleObject(bw, fieldName, valueToBeUpdated, propertyType, createdObjects);
            }
        }
    }
}
 Danilo Cianciulli18 июл. 2016 г., 13:05
Не ясно, как использовать JsonNode в моем методе обновления в классе Controller. Должны ли мои DTO наследовать от JsonNode?
 Danilo Cianciulli18 июл. 2016 г., 11:25
Ваш ответ кажется интересным, но я не очень хорошо его понял. Не могли бы вы дополнить это уместным примером?
 Martin Frey18 июл. 2016 г., 12:45
Какая часть, jsonnode requestmapping, служба слияния или проверка?
 Martin Frey19 июл. 2016 г., 08:25
Добавлен код, чтобы показать вам идею. Надеюсь, поможет :)
 Danilo Cianciulli19 июл. 2016 г., 10:26
Спасибо Мартин, ваше решение разумно, но оно доставляет мне неприятности из-за отсутствия автоматической проверки JSR-303 в DTO. С этой точки зрения, предложение @demon, пусть даже и менее сложное, подходит для ставок. Тем не менее, я рассмотрю ваше решение для других случаев :)

Используйте логические флаги какАвтор Джексона рекомендует.

class PersonDTO {
    private String firstName;
    private boolean isFirstNameDirty;

    public void setFirstName(String firstName){
        this.firstName = firstName;
        this.isFirstNameDirty = true;
    }

    public void getFirstName() {
        return firstName;
    }

    public boolean hasFirstName() {
        return isFirstNameDirty;
    }
}

Ваш ответ на вопрос