Перейти к основному содержимому

Работа с отношениями в Spring Data REST

· 10 мин. чтения

1. Обзор

В этом руководстве мы узнаем , как работать с отношениями между сущностями в Spring Data REST .

Мы сосредоточимся на ресурсах ассоциации, которые Spring Data REST предоставляет для репозитория, учитывая каждый тип отношений, которые мы можем определить.

Чтобы избежать дополнительной настройки, мы будем использовать для примеров встроенную базу данных H2 . Мы можем найти список необходимых зависимостей в нашей статье Introduction to Spring Data REST .

2. Отношения один к одному

2.1. Модель данных

Давайте определим два класса сущностей, Library и Address, имеющие отношение один к одному, используя аннотацию @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;

Если бы мы добавили вышеприведенное свойство secondAddress в класс Library , у нас было бы два ресурса с именами address , что привело бы к конфликту.

Мы можем решить эту проблему, указав другое значение для атрибута rel или опустив аннотацию RestResource , чтобы имя ресурса по умолчанию было secondAddress .

2.2. Репозитории

Чтобы представить эти объекты как ресурсы , мы создадим два интерфейса репозитория для каждого из них, расширив интерфейс CrudRepository :

public interface LibraryRepository extends CrudRepository<Library, Long> {}
public interface AddressRepository extends CrudRepository<Address, Long> {}

2.3. Создание ресурсов

Во-первых, мы добавим экземпляр библиотеки для работы:

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\"}"

В теле ответа мы видим, что ресурс ассоциации был открыт в конечной точке library/{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

Он должен вернуть объект библиотеки JSON с именем «Моя библиотека».

Чтобы удалить ассоциацию , мы можем вызвать конечную точку с помощью метода DELETE, обязательно используя ресурс ассоциации владельца отношения:

curl -i -X DELETE http://localhost:8080/libraries/1/libraryAddress

3. Связь «один ко многим»

Мы определяем отношение «один ко многим» с помощью аннотаций @OneToMany и @ManyToOne . Мы также можем добавить необязательную аннотацию @RestResource для настройки ресурса ассоциации.

3.1. Модель данных

Чтобы проиллюстрировать связь «один ко многим», мы добавим новую сущность « Книга» , которая представляет конец «многих» связи с сущностью « Библиотека »:

@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 List<Book> books;

//...

}

3.2. Репозиторий

Нам также нужно создать BookRepository :

public interface BookRepository extends CrudRepository<Book, Long> { }

3.3. Ресурсы Ассоциации

Чтобы добавить книгу в библиотеку , нам нужно сначала создать экземпляр книги , используя ресурс коллекции / books :

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 для ресурса ассоциации библиотеки/ книг :

curl -i -X GET http://localhost:8080/libraries/1/books

Возвращенный объект JSON будет содержать массив книг :

{
"_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, который имеет отношение «многие ко многим» с сущностью « Книга» :

@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 List<Book> books;

//standard constructors, getters, setters
}

Затем мы также добавим ассоциацию в класс Book :

public class Book {

//...

@ManyToMany(mappedBy = "books")
private List<Author> authors;

//...
}

4.2. Репозиторий

Далее мы создадим интерфейс репозитория для управления сущностью Author :

public interface AuthorRepository extends CrudRepository<Author, Long> { }

4.3. Ресурсы Ассоциации

Как и в предыдущих разделах, мы должны сначала создать ресурсы , прежде чем мы сможем установить ассоциацию.

Мы создадим экземпляр Author , отправив запрос POST в ресурс коллекции / authors :

curl -i -X POST -H "Content-Type:application/json" 
-d "{\"name\":\"author1\"}" http://localhost:8080/authors

Далее мы добавим вторую запись Book в нашу базу данных:

curl -i -X POST -H "Content-Type:application/json" 
-d "{\"title\":\"Book 2\"}" http://localhost:8080/books

Затем мы выполним запрос GET для нашей записи автора , чтобы просмотреть 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 , используя конечную точку author/1/books с методом PUT, который поддерживает медиа-тип text/uri-list и может получать более одного URI .

Чтобы отправить несколько URI , мы должны разделить их разрывом строки:

curl -i -X PUT -H "Content-Type:text/uri-list" 
--data-binary @uris.txt http://localhost:8080/authors/1/books

Файл uris.txt содержит URI книг, каждая в отдельной строке:

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"
}
}
}

Чтобы удалить ассоциацию , мы можем отправить запрос с помощью метода DELETE на 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 , который сохраняет объекты Library и Address , отправляя 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");
HttpEntity<String> httpEntity
= new HttpEntity<>(ADDRESS_ENDPOINT + "/1", requestHeaders);
template.exchange(LIBRARY_ENDPOINT + "/1/libraryAddress",
HttpMethod.PUT, httpEntity, String.class);

ResponseEntity<Library> libraryGetResponse
= template.getForEntity(ADDRESS_ENDPOINT + "/1/library", Library.class);
assertEquals("library is incorrect",
libraryGetResponse.getBody().getName(), LIBRARY_NAME);
}

5.2. Тестирование отношения «один ко многим»

Теперь мы создадим метод @Test , который сохраняет экземпляр Library и два экземпляра Book , отправляет запрос PUT на ресурс ассоциации /library каждого объекта Book и проверяет, сохранена ли связь: ``

@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");
HttpEntity<String> bookHttpEntity
= 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<Library> libraryGetResponse =
template.getForEntity(BOOK_ENDPOINT + "/1/library", Library.class);
assertEquals("library is incorrect",
libraryGetResponse.getBody().getName(), LIBRARY_NAME);
}

5.3. Тестирование отношения «многие ко многим»

Для проверки отношения «многие ко многим» между сущностями « Книга» и « Автор » мы создадим тестовый метод, который сохраняет одну запись « Автор » и две записи « Книга» .

Затем он отправляет запрос PUT ресурсу ассоциации /books с двумя URI Books и проверяет, что связь установлена: ``

@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");
HttpEntity<String> httpEntity = 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 .