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

Spring Метод MVC PATCH: частичные обновления

У меня есть проект, в котором я использую Spring MVC + Jackson для создания службы REST. Скажем, у меня есть следующий объект java

public class MyEntity {
    private Integer id;
    private boolean aBoolean;
    private String aVeryBigString;
    //getter & setters
}

Иногда я просто хочу обновить логическое значение, и я не думаю, что отправка всего объекта с его большой строкой - хорошая идея просто обновить простое булево значение. Итак, я рассмотрел использование метода PATCH HTTP для отправки только тех полей, которые необходимо обновить. Итак, я объявляю следующий метод в контроллере:

@RequestMapping(method = RequestMethod.PATCH)
public void patch(@RequestBody MyVariable myVariable) {
    //calling a service to update the entity
}

Проблема заключается в следующем: как узнать, какие поля необходимо обновить? Например, если клиент просто хочет обновить логическое значение, я получу объект с пустой "aVeryBigString". Как я должен знать, что пользователь просто хочет обновить логическое значение, но не хочет выпустить строку?

Я "решил" проблему, создав настраиваемые URL-адреса. Например, следующий URL: POST/myentities/1/aboolean/true будет сопоставлен методу, который позволяет только обновлять логическое значение. Проблема с этим решением заключается в том, что он не соответствует требованиям REST. Я не хочу быть совместимым с REST на 100%, но мне не очень удобно предоставлять настраиваемый URL-адрес для обновления каждого поля (особенно учитывая, что он вызывает проблемы, когда я хочу обновить несколько полей).

Другим решением было бы разделить "MyEntity" на несколько ресурсов и просто обновить эти ресурсы, но я чувствую, что это не имеет смысла: "MyEntity" есть простой ресурс, это не состоит из других ресурсов.

Итак, есть ли элегантный способ решения этой проблемы?

4b9b3361

Ответ 1

Вы можете изменить boolean на Boolean и присвоить значение NULL для всех полей, которые вы не хотите обновлять. Единственное значение, не равное нулевому значению, определит вас, какой из клиентов поля хочет обновить.

Ответ 2

Это может быть очень поздно, но ради новичков и людей, которые сталкиваются с одной и той же проблемой, позвольте мне поделиться с вами своим собственным решением.

В моих прошлых проектах, чтобы сделать это простым, я просто использую собственную Java-карту. Он будет захватывать все новые значения, включая нулевые значения, которые клиент явно установил в null. На этом этапе будет легко определить, какие свойства java должны быть установлены как null, в отличие от того, когда вы используете то же POJO, что и ваша модель домена, вы не сможете отличить, какие поля заданы клиентом для нулевого и которые просто не включены в обновление, но по умолчанию будут иметь значение null.

Кроме того, вам необходимо потребовать, чтобы http-запрос отправил идентификатор записи, которую вы хотите обновить, и не включайте ее в структуру данных патча. То, что я сделал, задает идентификатор в URL как переменную пути и данные патча как тело PATCH. Затем с идентификатором вы должны сначала записать запись через модель домена, а затем с помощью HashMap, вы можете просто использовать mapper service или утилиту для исправления изменений в соответствующей модели домена.

Обновление

Вы можете создать абстрактный суперкласс для своих сервисов с таким универсальным кодом, вы должны использовать Java Generics. Это всего лишь часть возможной реализации, я надеюсь, что вы получите идею. Также лучше использовать каркасную структуру, такую ​​как Orika или Dozer.

public abstract class AbstractService<Entity extends BaseEntity, DTO extends BaseDto> {
    @Autowired
    private MapperService mapper;

    @Autowired
    private BaseRepo<Entity> repo;

    private Class<DTO> dtoClass;

    private Class<Entity> entityCLass;

    public AbstractService(){
       entityCLass = (Class<Entity>) SomeReflectionTool.getGenericParameter()[0];
       dtoClass = (Class<DTO>) SomeReflectionTool.getGenericParameter()[1];
    }

    public DTO patch(Long id, Map<String, Object> patchValues) {
        Entity entity = repo.get(id);
        DTO dto = mapper.map(entity, dtoClass);
        mapper.map(patchValues, dto);
        Entity updatedEntity = toEntity(dto);
        save(updatedEntity);
        return dto;
    }
}

Ответ 3

Правильный способ сделать это - это способ, предложенный в JSON PATCH RFC 6902

Пример запроса:

PATCH http://example.com/api/entity/1 HTTP/1.1
Content-Type: application/json-patch+json 

[
  { "op": "replace", "path": "aBoolean", "value": true }
]

Ответ 4

Немного DomainObjectReader я нашел приемлемое решение, используя тот же подход, который используется Spring MVC DomainObjectReader см. Также: JsonPatchHandler

@RepositoryRestController
public class BookCustomRepository {
    private final DomainObjectReader domainObjectReader;
    private final ObjectMapper mapper;

    private final BookRepository repository;


    @Autowired
    public BookCustomRepository(BookRepository bookRepository, 
                                ObjectMapper mapper,
                                PersistentEntities persistentEntities,
                                Associations associationLinks) {
        this.repository = bookRepository;
        this.mapper = mapper;
        this.domainObjectReader = new DomainObjectReader(persistentEntities, associationLinks);
    }


    @PatchMapping(value = "/book/{id}", consumes = {MediaType.APPLICATION_JSON_UTF8_VALUE, MediaType.APPLICATION_JSON_VALUE})
    public ResponseEntity<?> patch(@PathVariable String id, ServletServerHttpRequest request) throws IOException {

        Book entityToPatch = repository.findById(id).orElseThrow(ResourceNotFoundException::new);
        Book patched = domainObjectReader.read(request.getBody(), entityToPatch, mapper);
        repository.save(patched);

        return ResponseEntity.noContent().build();
    }

}

Ответ 5

Весь смысл PATCH заключается в том, что вы не отправляете полное представление сущности, поэтому я не понимаю ваши комментарии о пустой строке. Вам придется обращаться с каким-то простым JSON, например:

{ aBoolean: true }

и примените это к указанному ресурсу. Идея заключается в том, что получено diff требуемого состояния ресурса и текущего состояния ресурса.

Ответ 6

Spring использует/не может использовать PATCH для исправления вашего объекта из-за той же самой проблемы, что и у вас: десериализатор JSON создает Java POJO с нулевыми полями.

Это означает, что вы должны предоставить собственную логику для исправления объекта (т.е. только при использовании PATCH, но не POST).

Либо вы знаете, что используете только не примитивные типы, либо некоторые правила (пустая строка null, которая не работает для всех), или вам нужно предоставить дополнительный параметр, который определяет переопределенные значения. Последний работает отлично для меня: приложение JavaScript знает, какие поля были изменены и отправлены в дополнение к телу JSON, который отображается на сервере. Например, если поле description было названо для изменения (patch), но не указано в теле JSON, оно было обнулено.

Ответ 7

Не могли бы вы просто отправить объект, состоящий из обновленных полей?

Script call:

var data = JSON.stringify({
                aBoolean: true
            });
$.ajax({
    type: 'patch',
    contentType: 'application/json-patch+json',
    url: '/myentities/' + entity.id,
    data: data
});

Spring Контроллер MVC:

@PatchMapping(value = "/{id}")
public ResponseEntity<?> patch(@RequestBody Map<String, Object> updates, @PathVariable("id") String id)
{
    // updates now only contains keys for fields that was updated
    return ResponseEntity.ok("resource updated");
}

В члене контроллера path выполните итерацию через пары ключ/значение на карте updates. В приведенном выше примере клавиша "aBoolean" будет содержать значение true. Следующим шагом будет фактически присвоить значения, вызвав средства настройки объектов. Однако это другая проблема.

Ответ 8

Вы можете использовать Optional<> для этого:

public class MyEntityUpdate {
    private Optional<String> aVeryBigString;
}

Таким образом, вы можете проверить объект обновления следующим образом:

if(update.getAVeryBigString() != null)
    entity.setAVeryBigString(update.getAVeryBigString().get());

Если поле aVeryBigString отсутствует в документе JSON, поле POJO aVeryBigString будет null. Если он находится в документе JSON, но со значением null, поле POJO будет Optional со значением null. Это решение позволяет вам различать случаи "без обновления" и "установка на ноль".

Ответ 9

Я исправил проблему вроде этого, потому что я не могу изменить службу

public class Test {

void updatePerson(Person person,PersonPatch patch) {

    for (PersonPatch.PersonPatchField updatedField : patch.updatedFields) {
        switch (updatedField){

            case firstname:
                person.setFirstname(patch.getFirstname());
                continue;
            case lastname:
                person.setLastname(patch.getLastname());
                continue;
            case title:
                person.setTitle(patch.getTitle());
                continue;
        }

    }

}

public static class PersonPatch {

    private final List<PersonPatchField> updatedFields = new ArrayList<PersonPatchField>();

    public List<PersonPatchField> updatedFields() {
        return updatedFields;
    }

    public enum PersonPatchField {
        firstname,
        lastname,
        title
    }

    private String firstname;
    private String lastname;
    private String title;

    public String getFirstname() {
        return firstname;
    }

    public void setFirstname(final String firstname) {
        updatedFields.add(PersonPatchField.firstname);
        this.firstname = firstname;
    }

    public String getLastname() {
        return lastname;
    }

    public void setLastname(final String lastname) {
        updatedFields.add(PersonPatchField.lastname);
        this.lastname = lastname;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(final String title) {
        updatedFields.add(PersonPatchField.title);
        this.title = title;
    }
}

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

Ответ 10

Вот реализация для команды patch с использованием googles GSON.

package de.tef.service.payment;

import com.google.gson.*;

class JsonHelper {
    static <T> T patch(T object, String patch, Class<T> clazz) {
        JsonElement o = new Gson().toJsonTree(object);
        JsonObject p = new JsonParser().parse(patch).getAsJsonObject();
        JsonElement result = patch(o, p);
        return new Gson().fromJson(result, clazz);
    }

    static JsonElement patch(JsonElement object, JsonElement patch) {
        if (patch.isJsonArray()) {
            JsonArray result = new JsonArray();
            object.getAsJsonArray().forEach(result::add);
            return result;
        } else if (patch.isJsonObject()) {
            System.out.println(object + " => " + patch);
            JsonObject o = object.getAsJsonObject();
            JsonObject p = patch.getAsJsonObject();
            JsonObject result = new JsonObject();
            o.getAsJsonObject().entrySet().stream().forEach(e -> result.add(e.getKey(), p.get(e.getKey()) == null ? e.getValue() : patch(e.getValue(), p.get(e.getKey()))));
            return result;
        } else if (patch.isJsonPrimitive()) {
            return patch;
        } else if (patch.isJsonNull()) {
            return patch;
        } else {
            throw new IllegalStateException();
        }
    }
}

Реализация рекурсивная, чтобы заботиться о вложенных структурах. Массивы не объединены, потому что у них нет ключа для слияния.

"Патч" JSON напрямую преобразуется из String в JsonElement, а не к объекту, чтобы сохранить заполненные поля отдельно от полей, заполненных NULL.

Ответ 11

Я заметил, что многие из предоставленных ответов - все исправления JSON или неполные ответы. Ниже приведено полное объяснение и пример того, что вам нужно для работы с реальным кодом.

Полная функция исправления:

@ApiOperation(value = "Patch an existing claim with partial update")
@RequestMapping(value = CLAIMS_V1 + "/{claimId}", method = RequestMethod.PATCH)
ResponseEntity<Claim> patchClaim(@PathVariable Long claimId, @RequestBody Map<String, Object> fields) {

    // Sanitize and validate the data
    if (claimId <= 0 || fields == null || fields.isEmpty() || !fields.get("claimId").equals(claimId)){
        return new ResponseEntity<>(HttpStatus.BAD_REQUEST); // 400 Invalid claim object received or invalid id or id does not match object
    }

    Claim claim = claimService.get(claimId);

    // Does the object exist?
    if( claim == null){
        return new ResponseEntity<>(HttpStatus.NOT_FOUND); // 404 Claim object does not exist
    }

    // Remove id from request, we don't ever want to change the id.
    // This is not necessary,
    // loop used below since we checked the id above
    fields.remove("claimId");

    fields.forEach((k, v) -> {
        // use reflection to get field k on object and set it to value v
        // Change Claim.class to whatver your object is: Object.class
        Field field = ReflectionUtils.findField(Claim.class, k); // find field in the object class
        field.setAccessible(true); 
        ReflectionUtils.setField(field, claim, v); // set given field for defined object to value V
    });

    claimService.saveOrUpdate(claim);
    return new ResponseEntity<>(claim, HttpStatus.OK);
}

Вышесказанное может сбить с толку некоторых людей, поскольку новые разработчики обычно не имеют дело с таким отражением. По сути, независимо от того, что вы передадите этой функции в теле, она найдет соответствующее утверждение, используя заданный идентификатор, а затем ТОЛЬКО обновит поля, которые вы передаете, как пару ключ-значение.

Пример тела:

PATCH/претензии /7

{
   "claimId":7,
   "claimTypeId": 1,
   "claimStatus": null
}

Вышеприведенное приведет к обновлению ApplicTypeId и ClausStatus до указанных значений для пункта 7, оставляя все остальные значения без изменений.

Так что возвращение будет примерно таким:

{
   "claimId": 7,
   "claimSrcAcctId": 12345678,
   "claimTypeId": 1,
   "claimDescription": "The vehicle is damaged beyond repair",
   "claimDateSubmitted": "2019-01-11 17:43:43",
   "claimStatus": null,
   "claimDateUpdated": "2019-04-09 13:43:07",
   "claimAcctAddress": "123 Sesame St, Charlotte, NC 28282",
   "claimContactName": "Steve Smith",
   "claimContactPhone": "777-555-1111",
   "claimContactEmail": "[email protected]",
   "claimWitness": true,
   "claimWitnessFirstName": "Stan",
   "claimWitnessLastName": "Smith",
   "claimWitnessPhone": "777-777-7777",
   "claimDate": "2019-01-11 17:43:43",
   "claimDateEnd": "2019-01-11 12:43:43",
   "claimInvestigation": null,
   "scoring": null
}

Как вы можете видеть, полный объект вернулся бы без изменения каких-либо данных, кроме того, что вы хотите изменить. Я знаю, что здесь есть небольшое повторение, я просто хотел обрисовать его четко.

Ответ 12

Мой ответ может быть запоздалым, но если он все еще сталкивается с той же проблемой. Я работал с PATCH со всеми возможными решениями, но не смог частично обновить поля объекта. Поэтому я переключился на POST и с постом, я могу обновлять определенные поля, не меняя значения неизменных полей.