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/librariesAPI возвращает объект 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 List books;
//...
} 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 List books;
//standard constructors, getters, setters
} Давайте также добавим ассоциацию в класс Book :
public class Book {
//...
@ManyToMany(mappedBy = "books")
private List authors;
//...
} 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");
HttpEntity httpEntity
= 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");
HttpEntity 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 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");
HttpEntity 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 .