1. Обзор
В этой статье мы рассмотрим как работать с отношениями между сущностями в Spring Data REST .
Мы сосредоточимся на ассоциативных ресурсах, которые предоставляет Spring Data REST для репозитория, с учетом каждого типа отношений, которые могут быть определены.
Чтобы избежать каких-либо дополнительных настроек, мы будем использовать для примеров встроенную базу данных H2 . Список необходимых зависимостей вы можете увидеть в нашей статье Введение в Spring Data REST .
И, если вы хотите сначала начать работу с Spring Data REST – вот хороший способ начать работу:
2. Отношения Один на один
2.1. Модель Данных
Давайте определим два класса сущностей Библиотека и Адрес , имеющие отношение один к одному, используя аннотацию @OneToOne . Ассоциация принадлежит Библиотеке конец ассоциации:
@Entity public class Library { @Id @GeneratedValue private long id; @Column private String name; @OneToOne @JoinColumn(name = "address_id") @RestResource(path = "libraryAddress", rel="address") private Address address; // standard constructor, getters, setters }
@Entity public class Address { @Id @GeneratedValue private long id; @Column(nullable = false) private String location; @OneToOne(mappedBy = "address") private Library library; // standard constructor, getters, setters }
Аннотация @RestResource является необязательной и может использоваться для настройки конечной точки.
Мы должны быть осторожны, чтобы иметь разные имена для каждого ресурса ассоциации . В противном случае мы столкнемся с JsonMappingException с сообщением: “Обнаружено несколько связей ассоциации с одним и тем же типом отношения! Неоднозначная ассоциация” .
Имя ассоциации по умолчанию соответствует имени свойства и может быть настроено с помощью атрибута rel аннотации @RestResource :
@OneToOne @JoinColumn(name = "secondary_address_id") @RestResource(path = "libraryAddress", rel="address") private Address secondaryAddress;
Если бы мы добавили свойство secondary Address выше в класс Library , у нас было бы два ресурса с именем address , и мы столкнулись бы с конфликтом.
Мы можем решить эту проблему, указав другое значение для атрибута rel или опустив аннотацию RestResource , чтобы имя ресурса по умолчанию было secondaryAddress .
2.2. Репозитории
Чтобы представить эти сущности в качестве ресурсов , давайте создадим два интерфейса репозитория для каждого из них, расширив интерфейс CrudRepository :
public interface LibraryRepository extends CrudRepository{}
public interface AddressRepository extends CrudRepository {}
2.3. Создание ресурсов
Во-первых, давайте добавим экземпляр Library для работы:
curl -i -X POST -H "Content-Type:application/json" -d '{"name":"My Library"}' http://localhost:8080/libraries
API возвращает объект JSON:
{ "name" : "My Library", "_links" : { "self" : { "href" : "http://localhost:8080/libraries/1" }, "library" : { "href" : "http://localhost:8080/libraries/1" }, "address" : { "href" : "http://localhost:8080/libraries/1/libraryAddress" } } }
Обратите внимание, что если вы используете curl в Windows, вам необходимо экранировать символ двойной кавычки внутри строки |, представляющей тело JSON :
-d "{\"name\":\"My Library\"}"
В теле ответа мы видим, что ресурс ассоциации был открыт в конечной точке libraries/{libraryId}/address .
Прежде чем мы создадим ассоциацию, отправка запроса GET в эту конечную точку вернет пустой объект.
Однако, если мы хотим добавить ассоциацию, мы также должны сначала создать экземпляр Address :
curl -i -X POST -H "Content-Type:application/json" -d '{"location":"Main Street nr 5"}' http://localhost:8080/addresses
Результатом запроса POST является объект JSON, содержащий запись Address :
{ "location" : "Main Street nr 5", "_links" : { "self" : { "href" : "http://localhost:8080/addresses/1" }, "address" : { "href" : "http://localhost:8080/addresses/1" }, "library" : { "href" : "http://localhost:8080/addresses/1/library" } } }
2.4. Создание Ассоциаций
После сохранения обоих экземпляров мы можем установить связь, используя один из ресурсов ассоциации .
Это делается с помощью HTTP-метода PUT, который поддерживает тип носителя text/uri-list и тело , содержащее URI ресурса для привязки к ассоциации.
Поскольку объект Library является владельцем ассоциации, давайте добавим адрес в библиотеку:
curl -i -X PUT -d "http://localhost:8080/addresses/1" -H "Content-Type:text/uri-list" http://localhost:8080/libraries/1/libraryAddress
В случае успеха это возвращает статус 204. Чтобы проверить, давайте проверим библиотеку ассоциативный ресурс адреса :
curl -i -X GET http://localhost:8080/addresses/1/library
Это должно вернуть объект Library JSON с именем “Моя библиотека” .
Чтобы удалить связь , мы можем вызвать конечную точку с помощью метода DELETE, убедившись, что используем ресурс связи владельца связи:
curl -i -X DELETE http://localhost:8080/libraries/1/libraryAddress
3. Отношения “Один ко многим”
Отношение “один ко многим” определяется с помощью аннотаций @OneToMany и @ManyToOne и может иметь необязательную аннотацию @RestResource для настройки ресурса ассоциации.
3.1. Модель Данных
Чтобы проиллюстрировать отношение “один ко многим”, давайте добавим новую сущность Book , которая будет представлять конец “многих” отношений с сущностью Library :
@Entity public class Book { @Id @GeneratedValue private long id; @Column(nullable=false) private String title; @ManyToOne @JoinColumn(name="library_id") private Library library; // standard constructor, getter, setter }
Давайте также добавим отношение к классу Library :
public class Library { //... @OneToMany(mappedBy = "library") private Listbooks; //... }
3.2. Репозиторий
Нам также необходимо создать Хранилище книг :
public interface BookRepository extends CrudRepository{ }
3.3. Ресурсы Ассоциации
Чтобы добавить книгу в библиотеку , нам нужно сначала создать экземпляр Book , используя ресурс/|/books collection:
curl -i -X POST -d "{\"title\":\"Book1\"}" -H "Content-Type:application/json" http://localhost:8080/books
А вот ответ на запрос POST:
{ "title" : "Book1", "_links" : { "self" : { "href" : "http://localhost:8080/books/1" }, "book" : { "href" : "http://localhost:8080/books/1" }, "bookLibrary" : { "href" : "http://localhost:8080/books/1/library" } } }
В теле ответа мы видим, что конечная точка ассоциации /books/{BookID}/library была создана.
Давайте свяжем книгу с библиотекой , созданной в предыдущем разделе, отправив запрос PUT на ресурс ассоциации, содержащий URI ресурса библиотеки:
curl -i -X PUT -H "Content-Type:text/uri-list" -d "http://localhost:8080/libraries/1" http://localhost:8080/books/1/library
Мы можем проверить книги в библиотеке с помощью метода GET на ресурсе библиотеки/|/books association:
curl -i -X GET http://localhost:8080/libraries/1/books
Возвращаемый объект JSON будет содержать массив books :
{ "_embedded" : { "books" : [ { "title" : "Book1", "_links" : { "self" : { "href" : "http://localhost:8080/books/1" }, "book" : { "href" : "http://localhost:8080/books/1" }, "bookLibrary" : { "href" : "http://localhost:8080/books/1/library" } } } ] }, "_links" : { "self" : { "href" : "http://localhost:8080/libraries/1/books" } } }
Чтобы удалить ассоциацию , мы можем использовать метод DELETE на ресурсе ассоциации:
curl -i -X DELETE http://localhost:8080/books/1/library
4. Отношения “Многие ко многим”
Отношение “многие ко многим” определяется с помощью аннотации @ManyToMany , к которой мы можем добавить @RestResource .
4.1. Модель Данных
Чтобы создать пример отношения “многие ко многим”, давайте добавим новый класс модели Author , который будет иметь отношение “многие ко многим” с сущностью Book :
@Entity public class Author { @Id @GeneratedValue private long id; @Column(nullable = false) private String name; @ManyToMany(cascade = CascadeType.ALL) @JoinTable(name = "book_author", joinColumns = @JoinColumn(name = "book_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "author_id", referencedColumnName = "id")) private Listbooks; //standard constructors, getters, setters }
Давайте также добавим ассоциацию в класс Book :
public class Book { //... @ManyToMany(mappedBy = "books") private Listauthors; //... }
4.2. Репозиторий
Давайте создадим интерфейс репозитория для управления сущностью Author :
public interface AuthorRepository extends CrudRepository{ }
4.3. Ресурсы Ассоциации
Как и в предыдущих разделах, мы должны сначала создать ресурсы , прежде чем мы сможем создать ассоциацию.
Давайте сначала создадим экземпляр Author , отправив запросы POST на ресурс/ authors collection:
curl -i -X POST -H "Content-Type:application/json" -d "{\"name\":\"author1\"}" http://localhost:8080/authors
Далее, давайте добавим вторую Книгу запись в нашу базу данных:
curl -i -X POST -H "Content-Type:application/json" -d "{\"title\":\"Book 2\"}" http://localhost:8080/books
Давайте выполним запрос GET для нашей записи Author , чтобы просмотреть URL-адрес ассоциации:
{ "name" : "author1", "_links" : { "self" : { "href" : "http://localhost:8080/authors/1" }, "author" : { "href" : "http://localhost:8080/authors/1" }, "books" : { "href" : "http://localhost:8080/authors/1/books" } } }
Теперь мы можем создать ассоциацию между двумя записями Book и записью Author , используя конечную точку authors/1/books с помощью метода PUT, который поддерживает тип носителя text/uri-list и может получать более одного URI .
Чтобы отправить несколько URI s, мы должны разделить их разрывом строки:
curl -i -X PUT -H "Content-Type:text/uri-list" --data-binary @uris.txt http://localhost:8080/authors/1/books
В uris.txt файл содержит URL s книг, каждая из которых находится в отдельной строке:
http://localhost:8080/books/1 http://localhost:8080/books/2
Чтобы убедиться , что обе книги были связаны с автором , мы можем отправить запрос GET в конечную точку ассоциации:
curl -i -X GET http://localhost:8080/authors/1/books
И мы получаем такой ответ:
{ "_embedded" : { "books" : [ { "title" : "Book 1", "_links" : { "self" : { "href" : "http://localhost:8080/books/1" } //... } }, { "title" : "Book 2", "_links" : { "self" : { "href" : "http://localhost:8080/books/2" } //... } } ] }, "_links" : { "self" : { "href" : "http://localhost:8080/authors/1/books" } } }
Чтобы удалить ассоциацию , мы можем отправить запрос с методом УДАЛЕНИЯ на URL-адрес ресурса ассоциации, за которым следует {BookID} :
curl -i -X DELETE http://localhost:8080/authors/1/books/1
5. Тестирование конечных точек С помощью TestRestTemplate
Давайте создадим тестовый класс, который вводит экземпляр TestRestTemplate и определяет константы, которые мы будем использовать:
@RunWith(SpringRunner.class) @SpringBootTest(classes = SpringDataRestApplication.class, webEnvironment = WebEnvironment.DEFINED_PORT) public class SpringDataRelationshipsTest { @Autowired private TestRestTemplate template; private static String BOOK_ENDPOINT = "http://localhost:8080/books/"; private static String AUTHOR_ENDPOINT = "http://localhost:8080/authors/"; private static String ADDRESS_ENDPOINT = "http://localhost:8080/addresses/"; private static String LIBRARY_ENDPOINT = "http://localhost:8080/libraries/"; private static String LIBRARY_NAME = "My Library"; private static String AUTHOR_NAME = "George Orwell"; }
5.1. Тестирование отношений “Один к одному”
Давайте создадим метод @Test , который сохраняет Библиотеку и Адрес объекты, делая запросы POST к ресурсам коллекции.
Затем он сохраняет связь с запросом PUT к ресурсу ассоциации и проверяет, что она была установлена с запросом GET к тому же ресурсу:
@Test public void whenSaveOneToOneRelationship_thenCorrect() { Library library = new Library(LIBRARY_NAME); template.postForEntity(LIBRARY_ENDPOINT, library, Library.class); Address address = new Address("Main street, nr 1"); template.postForEntity(ADDRESS_ENDPOINT, address, Address.class); HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.add("Content-type", "text/uri-list"); HttpEntityhttpEntity = new HttpEntity<>(ADDRESS_ENDPOINT + "/1", requestHeaders); template.exchange(LIBRARY_ENDPOINT + "/1/libraryAddress", HttpMethod.PUT, httpEntity, String.class); ResponseEntity libraryGetResponse = template.getForEntity(ADDRESS_ENDPOINT + "/1/library", Library.class); assertEquals("library is incorrect", libraryGetResponse.getBody().getName(), LIBRARY_NAME); }
5.2. Тестирование отношения “Один ко многим”
Давайте создадим метод @Test , который сохраняет экземпляр Библиотеки и два экземпляра Книги , отправляет запрос PUT каждому ресурсу книги объекта библиотеки/| ассоциации и проверяет, что связь была сохранена:
@Test public void whenSaveOneToManyRelationship_thenCorrect() { Library library = new Library(LIBRARY_NAME); template.postForEntity(LIBRARY_ENDPOINT, library, Library.class); Book book1 = new Book("Dune"); template.postForEntity(BOOK_ENDPOINT, book1, Book.class); Book book2 = new Book("1984"); template.postForEntity(BOOK_ENDPOINT, book2, Book.class); HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.add("Content-Type", "text/uri-list"); HttpEntitybookHttpEntity = new HttpEntity<>(LIBRARY_ENDPOINT + "/1", requestHeaders); template.exchange(BOOK_ENDPOINT + "/1/library", HttpMethod.PUT, bookHttpEntity, String.class); template.exchange(BOOK_ENDPOINT + "/2/library", HttpMethod.PUT, bookHttpEntity, String.class); ResponseEntity libraryGetResponse = template.getForEntity(BOOK_ENDPOINT + "/1/library", Library.class); assertEquals("library is incorrect", libraryGetResponse.getBody().getName(), LIBRARY_NAME); }
5.3. Тестирование отношений “Многие ко многим”
Для тестирования отношения “многие ко многим” между сущностями Book и Author мы создадим метод тестирования, который сохранит одну запись Author и две записи Book .
Затем он отправляет запрос PUT ресурсу /books association с двумя Books ‘ URI s и проверяет, что связь установлена:
@Test public void whenSaveManyToManyRelationship_thenCorrect() { Author author1 = new Author(AUTHOR_NAME); template.postForEntity(AUTHOR_ENDPOINT, author1, Author.class); Book book1 = new Book("Animal Farm"); template.postForEntity(BOOK_ENDPOINT, book1, Book.class); Book book2 = new Book("1984"); template.postForEntity(BOOK_ENDPOINT, book2, Book.class); HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.add("Content-type", "text/uri-list"); HttpEntityhttpEntity = new HttpEntity<>( BOOK_ENDPOINT + "/1\n" + BOOK_ENDPOINT + "/2", requestHeaders); template.exchange(AUTHOR_ENDPOINT + "/1/books", HttpMethod.PUT, httpEntity, String.class); String jsonResponse = template .getForObject(BOOK_ENDPOINT + "/1/authors", String.class); JSONObject jsonObj = new JSONObject(jsonResponse).getJSONObject("_embedded"); JSONArray jsonArray = jsonObj.getJSONArray("authors"); assertEquals("author is incorrect", jsonArray.getJSONObject(0).getString("name"), AUTHOR_NAME); }
6. Заключение
В этом уроке мы продемонстрировали использование различных типов отношений с Spring Data REST.
Полный исходный код примеров можно найти на GitHub .