Рубрики
Без рубрики

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

Практическое руководство по работе с отношениями сущностей в Spring Data REST.

Автор оригинала: baeldung.

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 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 с двумя BooksURI 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 .