The new Certification Class of REST With Spring is out:

>> CHECK OUT THE COURSE

1. Overview

In this article, we’re going to take a look at how to work with relationships between entities in Spring Data REST.

We will focus on the association resources that Spring Data REST exposes for a repository, considering each type of relationship that can be defined.

New Guide: Microservices with

Spring Boot and Spring Cloud

To avoid any extra setup, we will use the H2 embedded database for the examples. You can see the list of required dependencies in our Introduction to Spring Data REST article.

2. One-to-One Relationship

2.1. The Data Model

Let’s define two entity classes Library and Address having a one-to-one relationship, using the @OneToOne annotation. The association is owned by the Library end of the 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
}

The @RestResource annotation is optional and can be used to customize the endpoint.

We must be careful to have different names for each association resource. Otherwise, we will encounter a JsonMappingException with the message: “Detected multiple association links with same relation type! Disambiguate association”.

The association name defaults to the property name and can be customized using the rel attribute of @RestResource annotation:

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

If we were to add the secondaryAddress property above to the Library class, we would have two resources named address, and we would encounter a conflict.

We can resolve this by specifying a different value for the rel attribute or by omitting the RestResource annotation so that the resource name defaults to secondaryAddress.

2.2. The Repositories

In order to expose these entities as resources, let’s create two repository interfaces for each of them, by extending the CrudRepository interface:

public interface LibraryRepository extends CrudRepository<Library, Long> {}
public interface AddressRepository extends CrudRepository<Address, Long> {}

2.3. Creating the Resources

First, let’s add a Library instance to work with:

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

The API returns the JSON object:

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

Note that if you’re using curl on Windows, you have to escape the double-quote character inside the String that represents the JSON body:

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

We can see in the response body that an association resource has been exposed at the libraries/{libraryId}/address endpoint.

Before we create an association, sending a GET request to this endpoint will return an empty object.

However, if we want to add an association, we must first create an Address instance also:

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

The result of the POST request is a JSON object containing the Address record:

{
  "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. Creating the Associations

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

This is done using the HTTP method PUT, which supports a media type of text/uri-list, and a body containing the URI of the resource to bind to the association.

Since the Library entity is the owner of the association, let’s add an address to a library:

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

If successful, this returns status 204. To verify, let’s check the library association resource of the address:

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

This should return the Library JSON object with name “My Library”.

To remove an association, we can call the endpoint with DELETE method, making sure to use the association resource of the owner of the relationship:

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

3. One-to-Many Relationship

A one-to-many relationship is defined using the @OneToMany and @ManyToOne annotations and can have the optional @RestResource annotation to customize the association resource.

3.1. The Data Model

To exemplify a one-to-many relationship, let’s add a new Book entity that will represent the “many” end of a relationship with the Library entity:

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

Let’s add the relationship to the Library class as well:

public class Library {
 
    //...
 
    @OneToMany(mappedBy = "library")
    private List<Book> books;
 
    //...
 
}

3.2. The Repository

We also need to create a BookRepository:

public interface BookRepository extends CrudRepository<Book, Long> { }

3.3. The Association Resources

In order to add a book to a library, we need to create a Book instance first by using the /books collection resource:

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

And here is the response from the POST request:

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

In the response body, we can see the association endpoint /books/{bookId}/library has been created.

Let’s associate the book with the library created in the previous section by sending a PUT request to the association resource that contains the URI of the library resource:

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

We can verify the books in the library by using GET method on the library’s /books association resource:

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

The returned JSON object will contain a books array:

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

To remove an association, we can use the DELETE method on the association resource:

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

4. Many-to-Many Relationship

A many-to-many relationship is defined using @ManyToMany annotation, to which we can add @RestResource.

4.1. The Data Model

To create an example of a many-to-many relationship, let’s add a new model class Author that will have a many-to-many relationship with the Book entity:

@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<Book> books;

    //standard constructors, getters, setters
}

Let’s add the association in the Book class as well:

public class Book {
 
    //...
 
    @ManyToMany(mappedBy = "books")
    private List<Author> authors;
 
    //...
}

4.2. The Repository

Let’s create a repository interface to manage the Author entity:

public interface AuthorRepository extends CrudRepository<Author, Long> { }

4.3. The Association Resources

As in the previous sections, we must first create the resources before we can establish the association.

Let’s first create an Author instance by sending a POST requests to the /authors collection resource:

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

Next, let’s add a second Book record to our database:

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

Let’s execute a GET request on our Author record to view the association 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"
    }
  }
}

Now we can create an association between the two Book records and the Author record using the endpoint authors/1/books with PUT method, which supports a media type of text/uri-list and can receive more than one URI.

To send multiple URIs we have to separate them by a line break:

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

The uris.txt file contains the URIs of the books, each on a separate line:

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

To verify both books have been associated with the author, we can send a GET request to the association endpoint:

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

And we receive this response:

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

To remove an association, we can send a request with DELETE method to the URL of the association resource followed by {bookId}:

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

5. Testing the Endpoints with TestRestTemplate

Let’s create a test class that injects a TestRestTemplate instance and defines the constants we will use:

@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. Testing the One-to-One Relationship

Let’s create a @Test method that saves Library and Address objects by making POST requests to the collection resources.

Then it saves the relationship with a PUT request to the association resource and verifies that it has been established with a GET request to the same resource:

@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<String> httpEntity 
      = new HttpEntity<>(ADDRESS_ENDPOINT + "/1", requestHeaders);
    template.exchange(LIBRARY_ENDPOINT + "/1/libraryAddress", 
      HttpMethod.PUT, httpEntity, String.class);

    ResponseEntity<Library> libraryGetResponse 
      = template.getForEntity(ADDRESS_ENDPOINT + "/1/library", Library.class);
    assertEquals("library is incorrect", 
      libraryGetResponse.getBody().getName(), LIBRARY_NAME);
}

5.2. Testing the One-to-Many Relationship

Let’s create a @Test method that saves a Library instance and two Book instances, sends a PUT request to each Book object’s /library association resource, and verifies that the relationship has been saved:

@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<String> 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<Library> libraryGetResponse = 
      template.getForEntity(BOOK_ENDPOINT + "/1/library", Library.class);
    assertEquals("library is incorrect", 
      libraryGetResponse.getBody().getName(), LIBRARY_NAME);
}

5.3. Testing the Many-to-Many Relationship

For testing the many-to-many relationship between Book and Author entities, we will create a test method that saves one Author record and two Book records.

Then it sends a PUT request to the /books association resource with the two BooksURIs and verifies that the relationship has been established:

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

In this tutorial, we have demonstrated the use of different types of relationships with Spring Data REST.

The full source code of the examples can be found over on GitHub.

Go deeper into building a REST API with Spring:

>> CHECK OUT THE CLASSES

  • Sunkara Karuna

    when we have a relation in the entity and when we use Page object , then its not returning releational (child table) data in the get response, same is returning when we are not using page object.

    For Example, in the Repository class if we have following 2 methods are declared.

    public interface BookRepository extends CrudRepository {

    List findByOne(int id);

    Page findByOne(int id, Pageable pageable);

    }

    when we invoke from REST Client, The first method is returning the Book object along with Author object details, Same is not returning with second method ( which is used Page object).

    Do we need to add anything to return relational data when we use page object ????

    • Grzegorz Piwowarek

      Well, that’s an interesting case. Have you tried using PagingAndSortingRepository instead of CrucRepository?

      • Sunkara Karuna

        when I read CrudRepository extends PagingAndSortingRepository so I didnt try, but when I extend Paging… also same output.

  • Виталий Карелин

    2.4 has an error
    curl -i -X PUT -d “http://localhost:8080/addresses/1” -H “Content-Type:text/uri-list” http://localhost:8080/libraries/1/libraryAddress