Trabalhando com relacionamentos no Spring Data REST

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