Utilisation de relations dans REST Spring Data

Utilisation de relations dans REST Data Spring

1. Vue d'ensemble

Dans cet article, nous allons jeter un œil àhow to work with relationships between entities in Spring Data REST.

Nous nous concentrerons sur les ressources d'association exposées par Spring Data REST pour un référentiel, en considérant chaque type de relation pouvant être défini.

Pour éviter toute configuration supplémentaire, nous utiliserons la base de données intégréeH2 pour les exemples. Vous pouvez voir la liste des dépendances requises dans notre articleIntroduction to Spring Data REST.

 

Et, si vous recherchez les premiersget started with Spring Data REST, voici un bon moyen de démarrer:

[.iframe-fluid] ##

2. Relation individuelle

2.1. Le modèle de données

Définissons deux classes d'entitésLibrary etAddress ayant une relation un-à-un, en utilisant l'annotation@OneToOne. L'association appartient à l'extrémitéLibrary de l'association:

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

L'annotation@RestResource est facultative et peut être utilisée pour personnaliser le point de terminaison.

We must be careful to have different names for each association resource. Sinon, nous rencontrerons unJsonMappingException avec le message:“Detected multiple association links with same relation type! Disambiguate association”.

Le nom de l'association par défaut est le nom de la propriété et peut être personnalisé à l'aide de l'attributrel de l'annotation@RestResource:

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

Si nous devions ajouter la propriétésecondaryAddress ci-dessus à la classeLibrary, nous aurions deux ressources nomméesaddress, et nous rencontrerions un conflit.

Nous pouvons résoudre ce problème en spécifiant une valeur différente pour l'attributrel ou en omettant l'annotationRestResource afin que le nom de la ressource par défaut soitsecondaryAddress.

2.2. Les référentiels

Pourexpose these entities as resources, créons deux interfaces de référentiel pour chacune d’entre elles, en étendant l’interfaceCrudRepository:

public interface LibraryRepository extends CrudRepository {}
public interface AddressRepository extends CrudRepository {}

2.3. Création des ressources

Tout d'abord, ajoutons une instanceLibrary avec laquelle travailler:

curl -i -X POST -H "Content-Type:application/json"
  -d '{"name":"My Library"}' http://localhost:8080/libraries

L'API renvoie l'objet 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"
    }
  }
}

Notez que si vous utilisezcurl sous Windows, vous devez échapper le caractère guillemet double à l'intérieur duString qui représente le corps deJSON:

-d "{\"name\":\"My Library\"}"

Nous pouvons voir dans le corps de la réponse qu'une ressource d'association a été exposée au point de terminaisonlibraries/{libraryId}/address.

Avant de créer une association, l'envoi d'une requête GET à ce point de terminaison renverra un objet vide.

Cependant, si nous voulons ajouter une association, nous devons d'abord créer une instanceAddress également:

curl -i -X POST -H "Content-Type:application/json"
  -d '{"location":"Main Street nr 5"}' http://localhost:8080/addresses

Le résultat de la requête POST est un objet JSON contenant l'enregistrementAddress:

{
  "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. Créer les associations

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

Cela se fait à l'aide de la méthode HTTP PUT, qui prend en charge un type de média detext/uri-list, et un corps contenant lesURI de la ressource à lier à l'association.

Étant donné que l’entitéLibrary est le propriétaire de l’association, ajoutons une adresse à une bibliothèque:

curl -i -X PUT -d "http://localhost:8080/addresses/1"
  -H "Content-Type:text/uri-list" http://localhost:8080/libraries/1/libraryAddress

En cas de succès, cela renvoie le statut 204. Pour vérifier, vérifions la ressource d’associationlibrary desaddress:

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

Cela devrait renvoyer l'objet JSONLibrary avec le nom“My Library”.

Pourremove an association, nous pouvons appeler le point de terminaison avec la méthode DELETE, en veillant à utiliser la ressource d'association du propriétaire de la relation:

curl -i -X DELETE http://localhost:8080/libraries/1/libraryAddress

3. Relation un à plusieurs

Une relation un-à-plusieurs est définie à l'aide des annotations@OneToMany et@ManyToOne et peut avoir l'annotation facultative@RestResource pour personnaliser la ressource d'association.

3.1. Le modèle de données

Pour illustrer une relation un-à-plusieurs, ajoutons une nouvelle entitéBook qui représentera la fin "plusieurs" d'une relation avec l'entité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
}

Ajoutons également la relation à la classeLibrary:

public class Library {

    //...

    @OneToMany(mappedBy = "library")
    private List books;

    //...

}

3.2. Le référentiel

Nous devons également créer unBookRepository:

public interface BookRepository extends CrudRepository { }

3.3. Les ressources de l'association

Pouradd a book to a library, nous devons d'abord créer une instance deBook en utilisant la ressource de collection /books:

curl -i -X POST -d "{\"title\":\"Book1\"}"
  -H "Content-Type:application/json" http://localhost:8080/books

Et voici la réponse de la demande 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"
    }
  }
}

Dans le corps de la réponse, nous pouvons voir que le point de terminaison de l'association/books/{bookId}/library a été créé.

Créonsassociate the book with the library dans la section précédente en envoyant une requête PUT à la ressource d’association qui contient lesURI de la ressource de bibliothèque:

curl -i -X PUT -H "Content-Type:text/uri-list"
-d "http://localhost:8080/libraries/1" http://localhost:8080/books/1/library

Nous pouvonsverify the books in the library en utilisant la méthode GET sur la ressource d’association /books de la bibliothèque:

curl -i -X GET http://localhost:8080/libraries/1/books

L'objet JSON retourné contiendra un tableaubooks:

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

Pourremove an association, nous pouvons utiliser la méthode DELETE sur la ressource d'association:

curl -i -X DELETE http://localhost:8080/books/1/library

4. Relation plusieurs à plusieurs

Une relation plusieurs-à-plusieurs est définie à l'aide de l'annotation@ManyToMany, à laquelle nous pouvons ajouter@RestResource.

4.1. Le modèle de données

Pour créer un exemple de relation plusieurs-à-plusieurs, ajoutons une nouvelle classe de modèleAuthor qui aura une relation plusieurs-à-plusieurs avec l’entité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
}

Ajoutons également l'association dans la classeBook:

public class Book {

    //...

    @ManyToMany(mappedBy = "books")
    private List authors;

    //...
}

4.2. Le référentiel

Créons une interface de référentiel pour gérer l’entitéAuthor:

public interface AuthorRepository extends CrudRepository { }

4.3. Les ressources de l'association

Comme dans les sections précédentes, nous devons d'abordcreate the resources avant de pouvoir établir l'association.

Commençons par créer une instanceAuthor en envoyant une requête POST à ​​la ressource de collection /authors:

curl -i -X POST -H "Content-Type:application/json"
  -d "{\"name\":\"author1\"}" http://localhost:8080/authors

Ensuite, ajoutons un deuxième enregistrementBook à notre base de données:

curl -i -X POST -H "Content-Type:application/json"
  -d "{\"title\":\"Book 2\"}" http://localhost:8080/books

Exécutons une requête GET sur notre enregistrementAuthor pour afficher l'URL de l'association:

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

Maintenant, nous pouvonscreate an association entre les deux enregistrementsBook et l'enregistrementAuthor en utilisant le point de terminaisonauthors/1/books avec la méthode PUT, qui prend en charge un type de support detext/uri-list et peut recevoir plus d'unURI.

Pour envoyer plusieursURIs, nous devons les séparer par un saut de ligne:

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

Le fichieruris.txt contient lesURIs des livres, chacun sur une ligne distincte:

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

Pourverify both books have been associated with the author, nous pouvons envoyer une requête GET au point de terminaison de l'association:

curl -i -X GET http://localhost:8080/authors/1/books

Et nous recevons cette réponse:

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

Pourremove an association, nous pouvons envoyer une requête avec la méthode DELETE à l'URL de la ressource d'association suivie de{bookId}:

curl -i -X DELETE http://localhost:8080/authors/1/books/1

5. Test des points finaux avecTestRestTemplate

Créons une classe de test qui injecte une instanceTestRestTemplate et définit les constantes que nous utiliserons:

@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. Tester la relation un-à-un

Créons une méthode@Test qui enregistre les objetsLibrary etAddress en adressant des requêtes POST aux ressources de la collection.

Ensuite, il enregistre la relation avec une demande PUT à la ressource d'association et vérifie qu'elle a été établie avec une demande GET à la même ressource:

@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. Tester la relation un-à-plusieurs

Créons une méthode@Test qui enregistre une instanceLibrary et deux instancesBook, envoie une requête PUT à la ressource d'association/library de chaque objetBook, et vérifie que la relation a été enregistrée:

@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. Test de la relation plusieurs-à-plusieurs

Pour tester la relation plusieurs-à-plusieurs entre les entitésBook etAuthor, nous allons créer une méthode de test qui sauvegarde un enregistrementAuthor et deux enregistrementsBook.

Ensuite, il envoie une requête PUT à la ressource d’association/books avec les deuxBooksURI et vérifie que la relation a été établie:

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

Dans ce tutoriel, nous avons démontré l'utilisation de différents types de relations avec Spring Data REST.

Le code source complet des exemples peut être trouvéover on GitHub.