Метод исправления Spring MVC: частичные обновления
У меня есть проект, в котором я использую 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 будет сопоставлен методу, который позволяет только обновить логическое значение. Проблема с этим решением заключается в том, что оно не соответствует покою. Я не хочу быть на 100% совместимым с REST, но я не чувствую себя комфортно с предоставлением пользовательского URL для обновления каждого поля (особенно учитывая, что это вызывает проблемы, когда я хочу обновить несколько полей).
Другим решением было бы разделить "MyEntity" на несколько ресурсов и просто обновить эти ресурсы, но я чувствую, что это не имеет смысла: "MyEntity" является простым ресурсом, он не состоит из других ресурсов. ресурсы.
Итак, существует ли элегантный способ решения этой проблемы?
10 ответов:
Вы можете изменить boolean на Boolean и присвоить значение null для всех полей, которые вы не хотите обновлять. Единственное значение not null определяет, какое поле клиент хочет обновить.
Это может быть очень поздно, но ради новичков и людей, которые сталкиваются с той же проблемой, позвольте мне поделиться своим собственным решением.
В моих прошлых проектах, чтобы упростить его, я просто использую родную карту java. Он будет захватывать все новые значения, включая значения null, которые клиент явно установил в null. На этом этапе будет легко определить, какие свойства java должны быть установлены как null, в отличие от того, когда вы используете тот же POJO, что и ваша доменная модель, вы не сможете различайте, какие поля установлены клиентом в null, а какие просто не включены в обновление, но по умолчанию будут иметь значение null.
Кроме того, вы должны потребовать, чтобы http-запрос отправлял идентификатор записи, которую вы хотите обновить, и не включал его в структуру данных исправлений. То, что я сделал, - это установил идентификатор в URL как переменную path, а данные патча-как тело патча.Затем с помощью идентификатора вы получите сначала запись через доменную модель, а затем с помощью хэш-карты, вы можете просто использовать картограф сервис или утилита для исправления изменений в соответствующей модели домена.
Обновить
Вы можете создать абстрактный суперкласс для ваших сервисов с помощью такого универсального кода, Вы должны использовать универсальные языки Java. Это всего лишь сегмент возможной реализации, надеюсь, вы поняли идею.Также лучше использовать картографические рамки, такие как Орика или бульдозер.
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; } }
Правильный способ сделать это-способ, предложенный в 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 } ]
Весь смысл
PATCHзаключается в том, что Вы Не посылаете представление всей сущности, поэтому я не понимаю ваших комментариев о пустой строке. Вы должны были бы обрабатывать какой-то простой JSON, такой как:{ aBoolean: true }И примените это к указанному ресурсу. Идея заключается в том, что то, что было получено, является разницей желаемого состояния ресурса и текущего состояния ресурса.
Spring не использует / не может использовать
Это означает, что вы должны предоставить собственную логику для исправления сущности (т. е. только при использованииPATCHдля исправления объекта из-за той же проблемы, что и у вас: десериализатор JSON создает JAVA POJO с обнуленными полями.PATCH, но неPOST).Либо вы знаете, что используете только не примитивные типы, либо некоторые правила (пустой строкой является
null, что не работает для всех), либо вы должны предоставить дополнительный параметр, который определяет переопределенный ценности. Последний из них отлично работает для меня: приложение JavaScript знает, какие поля были изменены и отправлены в дополнение к телу JSON, которое перечисляет сервер. Например, если полеdescriptionбыло названо для изменения (patch), но не задано в теле JSON, оно было обнулено.
Покопавшись немного, я нашел приемлемое решение, используя то же решение, которое в настоящее время используется пружинным 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(); } }
Не могли бы вы просто отправить объект, состоящий из полей, которые были обновлены?
Вызов скрипта:
var data = JSON.stringify({ aBoolean: true }); $.ajax({ type: 'patch', contentType: 'application/json-patch+json', url: '/myentities/' + entity.id, data: data });Пружинный контроллер 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. Следующим шагом будет фактическое присвоение значений путем вызова установщиков сущностей. Однако это совсем другая проблема.
Я исправил эту проблему, потому что я не могу изменить сервис
Джексон вызывается только тогда, когда существуют значения. Так что вы можете сохранить, какой сеттер был вызван.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; } }
Вот реализация команды 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.
Для этого можно использовать
Optional<>:public class MyEntityUpdate { private Optional<String> aVeryBigString; }Таким образом, вы можете проверить объект обновления следующим образом:
if(update.getAVeryBigString() != null) entity.setAVeryBigString(update.getAVeryBigString().get());Если поле
aVeryBigStringотсутствует в документе JSON, поле POJOaVeryBigStringбудетnull. Если он находится в документе JSON, но со значениемnull, поле POJO будетOptionalс завернутым значениемnull. Данное решение позволяет различать "без обновления" и "установить на нуль" случаях.
Comments