1. Overview

In this tutorial, we’ll learn how to work with relationships between entities in Spring Data REST.

We’ll focus on the association resources that Spring Data REST exposes for a repository, considering each type of relationship that we can define.

To avoid any extra setup, we’ll use the H2 embedded database for the examples. We can find 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 by 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 we can use it to customize the endpoint.

We must also be careful to have different names for each association resource. Otherwise, we’ll 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 we can customize it using the rel attribute of the @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’d have two resources named address, thus encountering 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, we’ll 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, we’ll add a Library instance to work with:

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

Then 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 we’re using curl on Windows, we 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:

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, we’ll 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, it’ll return status 204. To verify this, we can check the library association resource of the address:

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

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

To remove an association, we can call the endpoint with the 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

We define a one-to-many relationship using the @OneToMany and @ManyToOne annotations. We can also add the optional @RestResource annotation to customize the association resource.

3.1. The Data Model

To exemplify a one-to-many relationship, we’ll add a new Book entity, which represents 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
}

Then we’ll 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’s 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 that the association endpoint, /books/{bookId}/library, has been created.

Now let’s associate the book with the library we 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 the 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

We define a many-to-many relationship using the @ManyToMany annotation, to which we can also add @RestResource.

4.1. The Data Model

To create an example of a many-to-many relationship, we’ll add a new model class, Author, which has 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
}

Then we’ll add the association in the Book class as well:

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

4.2. The Repository

Next, we’ll 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.

We’ll create an Author instance by sending a POST request to the /authors collection resource:

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

Next, we’ll 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

Then we’ll 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 the 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 are 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’ll 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 the 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’ll 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

We’ll 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’s 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

Now we’ll 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’ll 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 article, we 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.