Итак, вам удалось настроить свой сервер со spring data rest и hibernate. Теперь надо понять, какие запросы и какие JSON сервер ждет для создания и изменения сущностей в БД. Для примера возьмем сущности абстрактного форума: тема Topic, ее статус Status, пользователь User, список поисковых тегов List<Tag>.
У каждой темы есть связь one-to-many к Post, т.е. у нее есть список ответов. Со статусом будет связь many-to-one. С пользователем будет связь many-to-one. C тегами связь many-to-many(для простоты, однонаправленная — только Topic знает список своих тегов). У каждой сущности есть id.
Теперь нужно понять, что от нас ждет сервер. Запросы я проверял в RestClient для FireFox (не забудьте выставить заголовок Content-Type: application/json).
Для простых сущностей тело запроса на создание такое (сущность Tag):
{ "text" : "rest" } |
Его надо послать в POST запросе на end point http://localhost:8080/server/tags . Список полей, разумеется, может быть расширен. Главное, чтоб имена совпадали с теми, которые ожидает сервер. Если вы не навешивали аннотации из jackson, то имена сопадают с именами полей сущности. В случае успеха в ответ придет ответ из семейства двухсотых, а в теле будет сериализованная созданная сущность. Из нее нам пригодится присвоенный id.
Аналогично создаются сущности Status, User.
А вот на сущности Topic надо задержаться. Для нее запрос будет таким:
{ "createDate" : "2016-10-11", "status" : "http://localhost:8080/server/statuses/10", "author" : "http://localhost:8080/server/author/3", "tags" : ["http://localhost:8080/server/tags/1", "http://localhost:8080/server/tags/2"] } |
Это снова POST запрос, при передаче списка сущностей для связи ManyToMany контент тип (content-type) должен быть text/uri-list. Вложенные сущности, которые мы используем по ссылке, уже должны быть созданы.
Для создания темы без тегов надо передать «tags» : [].
Если вам нужно изменить уже существующую сущность, то к вашим услугам запрос PATCH:
PATCH http://localhost:8080/server/tags/11 { "text" : "json" } |
Пересылаем только те поля, которые хотим изменить. В случае успеха придет опять-таки 2хх ответ и представление измененной сущности. Будьте оснорожны с массивами — если пошлете пустой, то он и сохранится в БД. Id в теле JSON включать не нужно, он берется из адреса запроса (в данном случае = 11).
Теперь по поводу сериализации на стороне клиента, если уж мы знаем, что от нас ждет сервер. Клиент построен на vaadin и spring на нем в виде зависимосей vaadin-spring spring-plugin-core и spring-hateoas.
Если не писать свой сериализатор, то поведение по умолчанию для сложных сущностей будет генерировать представление в виде JSON вроде такого:
{"active":null,"createDate":[2016,10,11],"topicStatus":{"active":true,"status":"[0xd0][0x9e][0xd1][0x82][0xd0][0xbc][0xd0][0xb5][0xd0][0xbd][0xd0][0xb5][0xd0][0xbd][0xd0][0xbe]","links":[{"rel":"self","href":"http://localhost:8080/server/api/topicStatuses/2"},{"rel":"topicStatus","href":"http://localhost:8080/server/api/topicStatuses/2"}],"id":2},"fullName":null,"member":{"active":true,"login":"tvegorov","name":"[0xd0][0x9f][0xd0][0xb5][0xd1][0x82][0xd1][0x80][0xd0][0xbe][0xd0][0xb2][0xd0][0xb0] [0xd0][0xa2][0xd0][0xb0][0xd1][0x82][0xd1][0x8c][0xd1][0x8f][0xd0][0xbd][0xd0][0xb0] [0xd0][0x98][0xd0][0xb2][0xd0][0xb0][0xd0][0xbd\0xd0][0xbe][0xd0][0xb2][0xd0][0xbd][0xd0][0xb0]","role":{"active":true,"role":"[0xd0][0x9e][0xd0][0xbf][0xd0][0xb5][0xd1][0x80][0xd0][0xb0][0xd1][0x82][0xd0][0xbe][0xd1][0x80] [0xd0][0x9e][0xd0][0x9f][0xd0][0xa1]","links":[],"id":1},"creator":{"active":true,"name":"[0xd0][0x92][0xd0][0x9e][0xd0][0xa7][0xd0][0x95][0xd0][0x9f][0xd0][0xa8][0xd0][0x98][0xd0][0x99]","index":"385274","address":"[0xd1][0x80][0xd0][0xb5][0xd1][0x81][0xd0][0xbf]. [0xd0][0x90][0xd0][0xb4][0xd1][0x8b][0xd0][0xb3][0xd0][0xb5][0xd1][0x8f] [0xd1][0x80]-[0xd0][0xbd] [0xd0][0xa2][0xd0][0xb5][0xd1][0x83][0xd1][0x87][0xd0][0xb5][0xd0][0xb6][0xd1][0x81][0xd0][0xba][0xd0][0xb8][0xd0][0xb9] [0xd0][0xb0][0xd1][0x83][0xd0][0xbb] [0xd0][0x92][0xd0][0xbe][0xd1][0x87][0xd0][0xb5][0xd0][0xbf][0xd1][0x88][0xd0][0xb8][0xd0][0xb9] [0xd1][0x83][0xd0][0xbb]. [0xd0][0x93][0xd0][0xb0][0xd0][0xb3][0xd0][0xb0][0xd1][0x80][0xd0][0xb8][0xd0][0xbd][0xd0][0xb0] [0xd0][0xb4][0xd0][0xbe][0xd0][0xbc] 3","tags":[{"active":true,"number":"[0xd0][0x90][0xd0][0xaf] 129","size":{"active":true,"size":"[0xd0][0xa1][0xd1][0x80][0xd0][0xb5][0xd0][0xb4][0xd0][0xbd][0xd1][0x8f][0xd1][0x8f]","links":[],"id":2},"status":{"active":true,"status":"[0xd0][0xa1][0xd0][0xb2][0xd0][0xbe][0xd0][0xb1][0xd0][0xbe][0xd0][0xb4][0xd0][0xb5][0xd0][0xbd]","links":[],"id":1},"releaseDate":null,"links":[{"rel":"self","href":"http://localhost:8080/server/api/tags/19"},{"rel":"tag","href":"http://localhost:8080/server/api/tags/19{?projection}"},{"rel":"size","href":"http://localhost:8080/server/api/tags/19/size"},{"rel":"creator","href":"http://localhost:8080/server/api/tags/19/creator"},{"rel":"status","href":"http://localhost:8080/server/api/tags/19/status"}],"id":19},{"active":true,"number":"[0xd0][0x90][0xd0][0xaf] 125","size":{"active":true,"size":"[0xd0][0x9c][0xd0][0xb0][0xd0][0xbb][0xd0][0xb5][0xd0][0xbd][0xd1][0x8c][0xd0][0xba][0xd0][0xb0][0xd1][0x8f]","links":[],"id":1},"status":{"active":true,"status":"[0xd0][0xa1][0xd0][0xb2][0xd0][0xbe][0xd0][0xb1][0xd0][0xbe][0xd0][0xb4][0xd0][0xb5][0xd0][0xbd]","links":[],"id":1},"releaseDate":null,"links":[{"rel":"self","href":"http://localhost:8080/server/api/tags/15"},{"rel":"tag","href":"http://localhost:8080/server/api/tags/15{?projection}"},{"rel":"size","href":"http://localhost:8080/server/api/tags/15/size"},{"rel":"creator","href":"http://localhost:8080/server/api/tags/15/creator"},{"rel":"status","href":"http://localhost:8080/server/api/tags/15/status"}],"id":15},{"active":true,"number":"[0xd0][0x90][0xd0][0xaf] 121","size":{"active":true,"size":"[0xd0][0x9c][0xd0][0xb0][0xd0][0xbb][0xd0][0xb5][0xd0][0xbd][0xd1][0x8c][0xd0][0xba][0xd0][0xb0][0xd1][0x8f]","links":[],"id":1},"status":{"active":true,"status":"[0xd0][0xa1][0xd0][0xb2][0xd0][0xbe][0xd0][0xb1][0xd0][0xbe][0xd0][0xb4][0xd0][0xb5][0xd0][0xbd]","links":[],"id":1},"releaseDate":null,"links":[{"rel":"self","href":"http://localhost:8080/server/api/tags/11"},{"rel":"tag","href":"http://localhost:8080/server/api/tags/11{?projection}"},{"rel":"size","href":"http://localhost:8080/server/api/tags/11/size"},{"rel":"creator","href":"http://localhost:8080/server/api/tags/11/creator"},{"rel":"status","href":"http://localhost:8080/server/api/tags/11/status"}],"id":11},{"active":true,"number":"[0xd0][0x90][0xd0][0xaf] 130","size":{"active":true,"size":"[0xd0][0x9c][0xd0][0xb0][0xd0][0xbb][0xd0][0xb5][0xd0][0xbd][0xd1][0x8c][0xd0][0xba][0xd0][0xb0][0xd1][0x8f]","links":[],"id":1},"status":{"active":true,"status":"[0xd0][0x97][0xd0][0xb0][0xd1][0x80][0xd0][0xb5][0xd0][0xb7][0xd0][0xb5][0xd1][0x80][0xd0][0xb2][0xd0][0xb8][0xd1][0x80][0xd0][0xbe][0xd0][0xb2][0xd0][0xb0][0xd0][0xbd] [0xd0][0xb8] [0xd0][0xbd][0xd0][0xb5] [0xd0][0xbe][0xd0][0xbf][0xd0][0xbb][0xd0][0xb0][0xd1][0x87][0xd0][0xb5][0xd0][0xbd]","links":[],"id":2},"releaseDate":null,"links":[{"rel":"self","href":"http://localhost:8080/server/api/tags/20"},{"rel":"tag","href":"http://localhost:8080/server/api/tags/20{?projection}"},{"rel":"size","href":"http://localhost:8080/server/api/tags/20/size"},{"rel":"creator","href":"http://localhost:8080/server/api/tags/20/creator"},{"rel":"status","href":"http://localhost:8080/server/api/tags/20/status"}],"id":20},{"active":true,"number":"[0xd0][0x90][0xd0][0xaf] 126","size":{"active":true,"size":"[0xd0][0x9c][0xd0][0xb0][0xd0][0xbb][0xd0][0xb5][0xd0][0xbd][0xd1][0x8c][0xd0][0xba][0xd0][0xb0][0xd1][0x8f]","links":[],"id":1},"status":{"active":true,"status":"[0xd0][0x97][0xd0][0xb0][0xd1][0x80][0xd0][0xb5][0xd0][0xb7][0xd0][0xb5][0xd1][0x80][0xd0][0xb2][0xd0][0xb8][0xd1][0x80][0xd0][0xbe][0xd0][0xb2][0xd0][0xb0][0xd0][0xbd] [0xd0][0xb8] [0xd0][0xbd][0xd0][0xb5] [0xd0][0xbe][0xd0][0xbf][0xd0][0xbb][0xd0][0xb0][0xd1][0x87][0xd0][0xb5][0xd0][0xbd]","links":[],"id":2},"releaseDate":null,"links":[{"rel":"self","href":"http://localhost:8080/server/api/tags/16"},{"rel":"tag","href":"http://localhost:8080/server/api/tags/16{?projection}"},{"rel":"size","href":"http://localhost:8080/server/api/tags/16/size"},{"rel":"creator","href":"http://localhost:8080/server/api/tags/16/creator"},{"rel":"status","href":"http://localhost:8080/server/api/tags/16/status"}],"id":16},{"active":true,"number":"[0xd0][0x90][0xd0][0xaf] 124","size":{"active":true,"size":"[0xd0][0x9c][0xd0][0xb0][0xd0][0xbb][0xd0][0xb5][0xd0][0xbd][0xd1][0x8c][0xd0][0xba][0xd0][0xb0][0xd1][0x8f]","links":[],"id":1},"status":{"active":true,"status":"[0xd0][0x97][0xd0][0xb0][0xd1][0x80][0xd0][0xb5][0xd0][0xb7][0xd0][0xb5][0xd1][0x80][0xd0][0xb2][0xd0][0xb8][0xd1][0x80][0xd0][0xbe][0xd0][0xb2][0xd0][0xb0][0xd0][0xbd] [0xd0][0xb8] [0xd0][0xbd][0xd0][0xb5] [0xd0][0xbe][0xd0][0xbf][0xd0][0xbb][0xd0][0xb0][0xd1][0x87][0xd0][0xb5][0xd0][0xbd]","links":[],"id":2},"releaseDate":null,"links":[{"rel":"self","href":"http://localhost:8080/server/api/tags/14"},{"rel":"tag","href":"http://localhost:8080/server/api/tags/14{?projection}"},{"rel":"size","href":"http://localhost:8080/server/api/tags/14/size"},{"rel":"creator","href":"http://localhost:8080/server/api/tags/14/creator"},{"rel":"status","href":"http://localhost:8080/server/api/tags/14/status"}],"id":14},{"active":true,"number":"[0xd0][0x90][0xd0][0xaf] 122","size":{"active":true,"size":"[0xd0][0xa1][0xd1][0x80][0xd0][0xb5][0xd0][0xb4][0xd0][0xbd][0xd1][0x8f][0xd1][0x8f]","links":[],"id":2},"status":{"active":true,"status":"[0xd0][0x97][0xd0][0xb0][0xd1][0x80][0xd0][0xb5][0xd0][0xb7][0xd0][0xb5][0xd1][0x80][0xd0][0xb2][0xd0][0xb8][0xd1][0x80][0xd0][0xbe][0xd0][0xb2][0xd0][0xb0][0xd0][0xbd] [0xd0][0xb8] [0xd0][0xbd][0xd0][0xb5] [0xd0][0xbe][0xd0][0xbf][0xd0][0xbb][0xd0][0xb0][0xd1][0x87][0xd0][0xb5][0xd0][0xbd]","links":[],"id":2},"releaseDate":null,"links":[{"rel":"self","href":"http://localhost:8080/server/api/tags/12"},{"rel":"tag","href":"http://localhost:8080/server/api/tags/12{?projection}"},{"rel":"size","href":"http://localhost:8080/server/api/tags/12/size"},{"rel":"creator","href":"http://localhost:8080/server/api/tags/12/creator"},{"rel":"status","href":"http://localhost:8080/server/api/tags/12/status"}],"id":12},{"active":true,"number":"[0xd0][0x90][0xd0][0xaf] 128","size":{"active":true,"size":"[0xd0][0x91][0xd0][0xbe][0xd0][0xbb][0xd1][0x8c][0xd1][0x88][0xd0][0xb0][0xd1][0x8f]","links":[],"id":3},"status":{"active":true,"status":"[0xd0][0x97][0xd0][0xb0][0xd1][0x80][0xd0][0xb5][0xd0][0xb7][0xd0][0xb5][0xd1][0x80][0xd0][0xb2][0xd0][0xb8][0xd1][0x80][0xd0][0xbe][0xd0][0xb2][0xd0][0xb0][0xd0][0xbd] [0xd0][0xb8] [0xd0][0xbe][0xd0][0xbf][0xd0][0xbb][0xd0][0xb0][0xd1][0x87][0xd0][0xb5][0xd0][0xbd]","links":[],"id":3},"releaseDate":[2016,11,1],"links":[{"rel":"self","href":"http://localhost:8080/server/api/tags/18"},{"rel":"tag","href":"http://localhost:8080/server/api/tags/18{?projection}"},{"rel":"size","href":"http://localhost:8080/server/api/tags/18/size"},{"rel":"creator","href":"http://localhost:8080/server/api/tags/18/creator"},{"rel":"status","href":"http://localhost:8080/server/api/tags/18/status"}],"id":18},{"active":true,"number":"[0xd0][0x90][0xd0][0xaf] 127","size":{"active":true,"size":"[0xd0][0xa1][0xd1][0x80][0xd0][0xb5][0xd0][0xb4][0xd0][0xbd][0xd1][0x8f][0xd1][0x8f]","links":[],"id":2},"status":{"active":true,"status":"[0xd0][0x97][0xd0][0xb0][0xd1][0x80][0xd0][0xb5][0xd0][0xb7][0xd0][0xb5][0xd1][0x80][0xd0][0xb2][0xd0][0xb8][0xd1][0x80][0xd0][0xbe][0xd0][0xb2][0xd0][0xb0][0xd0][0xbd] [0xd0][0xb8] [0xd0][0xbe][0xd0][0xbf][0xd0][0xbb][0xd0][0xb0][0xd1][0x87][0xd0][0xb5][0xd0][0xbd]","links":[],"id":3},"releaseDate":[2016,12,11],"links":[{"rel":"self","href":"http://localhost:8080/server/api/tags/17"},{"rel":"tag","href":"http://localhost:8080/server/api/tags/17{?projection}"},{"rel":"size","href":"http://localhost:8080/server/api/tags/17/size"},{"rel":"creator","href":"http://localhost:8080/server/api/tags/17/creator"},{"rel":"status","href":"http://localhost:8080/server/api/tags/17/status"}],"id":17},{"active":true,"number":"[0xd0][0x90][0xd0][0xaf] 123","size":{"active":true,"size":"[0xd0][0xa1][0xd1][0x80][0xd0][0xb5][0xd0][0xb4][0xd0][0xbd][0xd1][0x8f][0xd1][0x8f]","links":[],"id":2},"status":{"active":true,"status":"[0xd0][0x97][0xd0][0xb0][0xd1][0x80][0xd0][0xb5][0xd0][0xb7][0xd0][0xb5][0xd1][0x80][0xd0][0xb2][0xd0][0xb8][0xd1][0x80][0xd0][0xbe][0xd0][0xb2][0xd0][0xb0][0xd0][0xbd] [0xd0][0xb8] [0xd0][0xbe][0xd0][0xbf][0xd0][0xbb][0xd0][0xb0][0xd1][0x87][0xd0][0xb5][0xd0][0xbd]","links":[],"id":3},"releaseDate":[2016,12,10],"links":[{"rel":"self","href":"http://localhost:8080/server/api/tags/13"},{"rel":"tag","href":"http://localhost:8080/server/api/tags/13{?projection}"},{"rel":"size","href":"http://localhost:8080/server/api/tags/13/size"},{"rel":"creator","href":"http://localhost:8080/server/api/tags/13/creator"},{"rel":"status","href":"http://localhost:8080/server/api/tags/13/status"}],"id":13}],"links":[{"rel":"self","href":"http://localhost:8080/server/api/creators/2"},{"rel":"creator","href":"http://localhost:8080/server/api/creators/2"},{"rel":"tagList","href":"http://localhost:8080/server/api/creators/2/tagList"}],"id":2},"links":[{"rel":"self","href":"http://localhost:8080/server/api/members/2"},{"rel":"member","href":"http://localhost:8080/server/api/members/2{?projection}"},{"rel":"role","href":"http://localhost:8080/server/api/members/2/role"},{"rel":"creator","href":"http://localhost:8080/server/api/members/2/creator"}],"id":2},"quantity":1,"client":null,"cost":100,"tags":null,"links":[],"id":null} |
Взято из отладочного логгирования спринга, в квадратных скобках шестнадцатиричное представление русских букв.
Вложенность и количество ненужных данных можно посмотреть, отформатировав JSON тут: https://jsonformatter.curiousconcept.com/
Как видно из концовки, это тело запроса на создание («id»:null).
Из-за такого нагромождения сервер вряд ли сможет создать сущность — создатели spring data rest не предусмотрели обработку таких монстров. Я сталкивался с тем, что подобный JSON с воспринимался как запрос на редактирование существующей сущности — id извлекался из одной из вложенных сущностей. Хорошо, что у нее была оптимистическая блокировка с @Version, которая генерила ошибку.
Поэтому надо писать для сложных сущностей сериализатор и подключать его.
Пример:
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; public class TopicSerializer extends JsonSerializer<topic> { @Override public void serialize(Topic topic, JsonGenerator generator, SerializerProvider provider) throws IOException { generator.writeStartObject(); if (topic.getStatus() != null) { generator.writeStringField("status", topic.getStatus().getId().getHref()); } if (topic.getAuthor() != null) { generator.writeStringField("status", topic.getAuthor().getId().getHref()); } List<tag> list = topic.getTags(); if (list != null) { generator.writeFieldName("tags"); generator.writeStartArray(); // [ if (list.size() > 0) { for (Tag tag : list) { generator.writeString(tag.getId().getHref()); } } generator.writeEndArray(); // ] } generator.writeEndObject(); } } </tag></topic> |
Примечание 1. На стороне клиента классы с описанием сущностей наследуются от org.springframework.hateoas.ResourceSupport, что и позволяет работать в них со ссылками.
Примечание 2. Если вы хотите позволить передачу пустой строки (или пустого списка, как в примере) в JSON, то проверка должна быть, такой, чтоб отсекать только null:
if (topic.getString() != null) { generator.writeStringField("string", topic.getString()); } |
В случае, если нужно отсекать не только null, но и пустые строки и строки только из white space, можно сделать так:
import org.apache.commons.lang3.StringUtils; if (StringUtils.isNotBlank(topic.getString())) { generator.writeStringField("string", topic.getString()); } |
Регистрация сериализатора производится в классе-конфиге:
@Configuration public class Config { ... @Bean public RestOperations restOperations() { ... MappingJackson2HttpMessageConverter jsonHttpMessageConverter = new MappingJackson2HttpMessageConverter(); jsonHttpMessageConverter.getObjectMapper().configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); jsonHttpMessageConverter.getObjectMapper().configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); jsonHttpMessageConverter.getObjectMapper().registerModule(new JavaTimeModule()); SimpleModule module = new SimpleModule(); module.addSerializer(Topic.class, new TopicSerializer()); // other serializers and deserializers get registered jsonHttpMessageConverter.getObjectMapper().registerModule(module); ... } } |