Trabalhando com relacionamentos no Spring Data REST
1. Visão geral
Neste artigo, vamos dar uma olhada emhow to work with relationships between entities in Spring Data REST.
Vamos nos concentrar nos recursos de associação que o Spring Data REST expõe para um repositório, considerando cada tipo de relacionamento que pode ser definido.
Para evitar qualquer configuração extra, usaremos o banco de dados incorporadoH2 para os exemplos. Você pode ver a lista de dependências necessárias em nosso artigoIntroduction to Spring Data REST.
E, se você está procurando primeiroget started with Spring Data REST - aqui está uma boa maneira de começar a correr:
[.iframe-fluido] ##
2. Relacionamento Um-para-Um
2.1. O Modelo de Dados
Vamos definir duas classes de entidadeLibrary eAddress tendo um relacionamento um-para-um, usando a anotação@OneToOne. A associação pertence à extremidadeLibrary da associação:
@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
}
A anotação@RestResource é opcional e pode ser usada para personalizar o terminal.
We must be careful to have different names for each association resource. Caso contrário, encontraremos umJsonMappingException com a mensagem:“Detected multiple association links with same relation type! Disambiguate association”.
O nome da associação é padronizado para o nome da propriedade e pode ser personalizado usando o atributorel da anotação@RestResource:
@OneToOne
@JoinColumn(name = "secondary_address_id")
@RestResource(path = "libraryAddress", rel="address")
private Address secondaryAddress;
Se adicionássemos a propriedadesecondaryAddress acima à classeLibrary, teríamos dois recursos chamadosaddress e encontraríamos um conflito.
Podemos resolver isso especificando um valor diferente para o atributorel ou omitindo a anotaçãoRestResource para que o padrão do nome do recurso sejasecondaryAddress.
2.2. Os Repositórios
Paraexpose these entities as resources, vamos criar duas interfaces de repositório para cada um deles, estendendo a interfaceCrudRepository:
public interface LibraryRepository extends CrudRepository {}
public interface AddressRepository extends CrudRepository {}
2.3. Criando os Recursos
Primeiro, vamos adicionar uma instânciaLibrary para trabalhar com:
curl -i -X POST -H "Content-Type:application/json"
-d '{"name":"My Library"}' http://localhost:8080/libraries
A API retorna o objeto 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"
}
}
}
Observe que se você estiver usandocurl no Windows, deverá escapar o caractere de aspas duplas dentro deString que representa o corpo deJSON:
-d "{\"name\":\"My Library\"}"
Podemos ver no corpo da resposta que um recurso de associação foi exposto no ponto de extremidadelibraries/{libraryId}/address.
Antes de criarmos uma associação, o envio de uma solicitação GET para este endpoint retornará um objeto vazio.
No entanto, se quisermos adicionar uma associação, devemos primeiro criar uma instânciaAddress também:
curl -i -X POST -H "Content-Type:application/json"
-d '{"location":"Main Street nr 5"}' http://localhost:8080/addresses
O resultado da solicitação POST é um objeto JSON contendo o registroAddress:
{
"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. Criando as Associações
After persisting both instances, we can establish the relationship by using one of the association resources.
Isso é feito usando o método HTTP PUT, que suporta um tipo de mídiatext/uri-list, e um corpo contendoURI do recurso a ser vinculado à associação.
Como a entidadeLibrary é a proprietária da associação, vamos adicionar um endereço a uma biblioteca:
curl -i -X PUT -d "http://localhost:8080/addresses/1"
-H "Content-Type:text/uri-list" http://localhost:8080/libraries/1/libraryAddress
Se for bem-sucedido, isso retornará o status 204. Para verificar, vamos verificar o recurso de associaçãolibrary deaddress:
curl -i -X GET http://localhost:8080/addresses/1/library
Isso deve retornar o objeto JSONLibrary com o nome“My Library”.
Pararemove an association, podemos chamar o endpoint com o método DELETE, certificando-se de usar o recurso de associação do proprietário do relacionamento:
curl -i -X DELETE http://localhost:8080/libraries/1/libraryAddress
3. Relacionamento Um-para-Muitos
Um relacionamento um-para-muitos é definido usando as anotações@OneToManye@ManyToOne e pode ter a anotação@RestResource opcional para personalizar o recurso de associação.
3.1. O Modelo de Dados
Para exemplificar um relacionamento um-para-muitos, vamos adicionar uma nova entidadeBook que representará o final “muitos” de um relacionamento com a entidadeLibrary:
@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
}
Vamos adicionar o relacionamento à classeLibrary também:
public class Library {
//...
@OneToMany(mappedBy = "library")
private List books;
//...
}
3.2. O Repositório
Também precisamos criar umBookRepository:
public interface BookRepository extends CrudRepository { }
3.3. Os Recursos da Associação
Paraadd a book to a library, precisamos criar uma instânciaBook primeiro usando o recurso de coleção /books:
curl -i -X POST -d "{\"title\":\"Book1\"}"
-H "Content-Type:application/json" http://localhost:8080/books
E aqui está a resposta da solicitação 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"
}
}
}
No corpo da resposta, podemos ver que o ponto final da associação/books/{bookId}/library foi criado.
Vamos criarassociate the book with the library na seção anterior, enviando uma solicitação PUT para o recurso de associação que contémURI do recurso de biblioteca:
curl -i -X PUT -H "Content-Type:text/uri-list"
-d "http://localhost:8080/libraries/1" http://localhost:8080/books/1/library
Podemosverify the books in the library usando o método GET no recurso de associação da biblioteca /books:
curl -i -X GET http://localhost:8080/libraries/1/books
O objeto JSON retornado conterá uma matrizbooks:
{
"_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"
}
}
}
Pararemove an association, podemos usar o método DELETE no recurso de associação:
curl -i -X DELETE http://localhost:8080/books/1/library
4. Relacionamento muitos-para-muitos
Um relacionamento muitos-para-muitos é definido usando a anotação@ManyToMany, à qual podemos adicionar@RestResource.
4.1. O Modelo de Dados
Para criar um exemplo de relacionamento muitos para muitos, vamos adicionar uma nova classe de modeloAuthor que terá um relacionamento muitos para muitos com a entidadeBook:
@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
}
Vamos adicionar a associação na classeBook também:
public class Book {
//...
@ManyToMany(mappedBy = "books")
private List authors;
//...
}
4.2. O Repositório
Vamos criar uma interface de repositório para gerenciar a entidadeAuthor:
public interface AuthorRepository extends CrudRepository { }
4.3. Os Recursos da Associação
Como nas seções anteriores, devemos primeirocreate the resources antes de podermos estabelecer a associação.
Vamos primeiro criar uma instânciaAuthor enviando solicitações POST para o recurso de coleção /authors:
curl -i -X POST -H "Content-Type:application/json"
-d "{\"name\":\"author1\"}" http://localhost:8080/authors
A seguir, vamos adicionar um segundo registroBook ao nosso banco de dados:
curl -i -X POST -H "Content-Type:application/json"
-d "{\"title\":\"Book 2\"}" http://localhost:8080/books
Vamos executar uma solicitação GET em nosso registroAuthor para visualizar o URL de associação:
{
"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"
}
}
}
Agora podemoscreate an association entre os dois registrosBook e o registroAuthor usando o ponto de extremidadeauthors/1/books com o método PUT, que suporta um tipo de mídia detext/uri-liste pode recebem mais de umURI.
Para enviar váriosURIs, temos que separá-los por uma quebra de linha:
curl -i -X PUT -H "Content-Type:text/uri-list"
--data-binary @uris.txt http://localhost:8080/authors/1/books
O arquivouris.txt contémURIs dos livros, cada um em uma linha separada:
http://localhost:8080/books/1
http://localhost:8080/books/2
Paraverify both books have been associated with the author, podemos enviar uma solicitação GET para o endpoint da associação:
curl -i -X GET http://localhost:8080/authors/1/books
E nós recebemos esta resposta:
{
"_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"
}
}
}
Pararemove an association, podemos enviar uma solicitação com o método DELETE para a URL do recurso de associação seguido por{bookId}:
curl -i -X DELETE http://localhost:8080/authors/1/books/1
5. Testando os pontos finais comTestRestTemplate
Vamos criar uma classe de teste que injeta uma instânciaTestRestTemplate e define as constantes que usaremos:
@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. Testando o relacionamento um para um
Vamos criar um método@Test que salvaLibraryeAddress objetos fazendo solicitações POST para os recursos da coleção.
Em seguida, ele salva o relacionamento com uma solicitação PUT no recurso de associação e verifica se foi estabelecido com uma solicitação GET para o mesmo recurso:
@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. Testando a relação um-para-muitos
Vamos criar um método@Test que salva uma instânciaLibrary e duas instânciasBook, envia uma solicitação PUT para cada recurso de associação/library do objetoBook e verifica se o relacionamento foi salvo:
@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. Testando o relacionamento muitos para muitos
Para testar a relação muitos-para-muitos entre as entidadesBookeAuthor, criaremos um método de teste que salva um registroAuthore dois registrosBook.
Em seguida, ele envia uma solicitação PUT para o recurso de associação/books com os doisBooks ‘URIs e verifica se o relacionamento foi estabelecido:
@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. Conclusão
Neste tutorial, demonstramos o uso de diferentes tipos de relacionamentos com o Spring Data REST.
O código-fonte completo dos exemplos pode ser encontradoover on GitHub.