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

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

1. обзор

В этой статье мы рассмотримhow to work with relationships between entities in Spring Data REST.

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

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

 

И, если вы ищете первыйget started with Spring Data REST - вот хороший способ взяться за дело:

[.iframe-fluid] ##

2. Индивидуальные отношения

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

Давайте определим два класса сущностейLibrary иAddress, имеющих взаимно однозначное отношение, используя аннотацию@OneToOne. Ассоциация принадлежит концу ассоциацииLibrary:

@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 является необязательной и может использоваться для настройки конечной точки.

We must be careful to have different names for each association resource. В противном случае мы встретимJsonMappingException с сообщением:“Detected multiple association links with same relation type! Disambiguate association”.

По умолчанию в качестве имени ассоциации используется имя свойства, и его можно настроить с помощью атрибутаrel аннотации@RestResource:

@OneToOne
@JoinColumn(name = "secondary_address_id")
@RestResource(path = "libraryAddress", rel="address")
private Address secondaryAddress;

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

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

2.2. Хранилища

Чтобыexpose these entities as resources, давайте создадим два интерфейса репозитория для каждого из них, расширив интерфейс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, вы должны экранировать символ двойной кавычки внутриString, который представляет тело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. Создание ассоциаций

After persisting both instances, we can establish the relationship by using one of the association resources.

Это выполняется с помощью 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. Для проверки давайте проверим ресурс ассоциацииlibrary дляaddress:

curl -i -X GET http://localhost:8080/addresses/1/library

Это должно вернуть объект JSONLibrary с именем“My Library”.

Дляremove an association мы можем вызвать конечную точку с помощью метода 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. Репозиторий

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

public interface BookRepository extends CrudRepository { }

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

Дляadd a book to a library нам нужно сначала создать экземплярBook, используя ресурс коллекции /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 создана.

Пустьassociate the book with the 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

Мы можемverify the books in the library, используя метод GET для ресурса ассоциации /books библиотеки:

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

Дляremove an association мы можем использовать метод 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. Ресурсы ассоциации

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

Давайте сначала создадим экземпляр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 для нашей записи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"
    }
  }
}

Теперь мы можемcreate an association между двумя записямиBook и записьюAuthor, используя конечную точкуauthors/1/books с методом PUT, который поддерживает тип носителяtext/uri-list и может получить более одногоURI.

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

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

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

http://localhost:8080/books/1
http://localhost:8080/books/2

Вverify both books have been associated with the author мы можем отправить запрос 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"
    }
  }
}

Вremove an association мы можем отправить запрос методом 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 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, который сохраняет экземпляр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 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 с двумяBooksURIs и проверяет, что связь была установлена:

@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.

Полный исходный код примеров можно найтиover on GitHub.