Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create resource with unique many-to-many relationsships in Spring Data REST?

I'm using Spring Boot and trying to implement a many to many relationship between entities using Spring Data REST, wherein no duplicates of the dependent Author entity are created.

Given the following code, when I post an initial book, with an author, the author is created, the book is created, and the relationship (look up table entry) is created. When I post a second book, with the same author, the second book is created and it attempts to create a second author, which fails due to the unique constraint on the author name.

Would I would expect/want to occur, is that the second book is created, and a entry is made relating the second book to the original author record, not an attempt to create a second/duplicate author.

@Entity(name = 'book')
class Book {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    long id
    ...
    @ManyToMany(fetch = FetchType.EAGER, cascade = [CascadeType.ALL])
    @JoinTable(name = 'books_authors', joinColumns = @JoinColumn(name = 'book_id'),
        inverseJoinColumns = @JoinColumn(name = 'author_id'))
    @RestResource(exported = false)
    Set<Author> authors
}

@Entity(name = 'author')
@ToString
class Author {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    long id

    @Column(nullable = false, unique = true)
    String fullName

    @ManyToMany(mappedBy = "authors")
    Set<Book> books
}

@RepositoryRestResource(collectionResourceRel = "authors", path = "authors")
interface AuthorRepository extends PagingAndSortingRepository<Author, Long> { }

@RepositoryRestResource(collectionResourceRel = "books", path = "books")
interface BookRepository extends PagingAndSortingRepository<Book, Long> { }

$ curl -i -X POST -H "Content-Type:application/json" -d '{ "title" : "Book One",  "authors" { "fullName": "AuthorName"} }' ....
HTTP/1.1 201 Created

$ curl -i -X POST -H "Content-Type:application/json" -d '{ "title" : "Book Two",  "authors" { "fullName": "AuthorName"} }' ....

The second post results in a failure due to attempting to create a new author with duplicate fullName.

I've implemented the same relationship using Spring Data, without Spring Data REST. In it, I had controllers and services, and was able to override this in the createBook(Book book) method. But with Spring Data REST, I have no service to implement. I've tried implementing the same logic in AbstractRepositoryEventListener<Book>#onBeforeCreate() but end up with detached entity issues.

This seems like something I should be able to handle with just the JPA relationship definition, but I can't seem to get there.

like image 990
P. Deters Avatar asked Mar 01 '15 18:03

P. Deters


People also ask

How do you create a many-to-many relationship in Java?

In JPA we use the @ManyToMany annotation to model many-to-many relationships. This type of relationship can be unidirectional or bidirectional: In a unidirectional relationship only one entity in the relationship points the other. In a bidirectional relationship both entities point to each other.

What is difference between JpaRepository and CrudRepository?

CrudRepository provides CRUD functions. PagingAndSortingRepository provides methods to do pagination and sort records. JpaRepository provides JPA related methods such as flushing the persistence context and delete records in a batch.


1 Answers

You're slightly misunderstanding or misusing some of the concepts REST and Spring Data REST are based on. Let me start by interpreting the code you presented.

Fundamentals

As both Author and Book are managed by repositories, they're basically elevated from normal entities to aggregate roots. Aggregate roots in DDD are supposed to manage invariants within themselves and must not be tweaked as side effect of manipulating another aggregate. That's why Spring Data REST exposes dedicated resources for them by default and creates links (literally) between the two.

The other aspect I'd like to briefly elaborate on is the topic of identity in REST. It's pretty simple actually as it's quite explicit. Resources have identity - their URI (as the name suggests). So the only means a server has to identify identity is by presenting it URIs that are identical.

Your sample

So in your particular example, the server has no way to tell that the second Book you post is referring to the very same author. Basically because you're not referring, you're inlining, which makes the Author part of the Book aggregate which contradicts your approach to have repositories for both in the first place.

If you think about it, your example raises two questions that even complicate the matter:

  1. Why would you want to transfer all properties of the Author a second time? What you're basically trying to express is: "this book belongs to the author I already created" which you'd to by submitting the URI of the first author created as value for the author property of the second Book to be created.

  2. What is supposed to happen if the second inlined Author document also changes some attributes? This basically contradicts the assumption for aggregates I outlined in the first place and leads back to question 1: you want to refer to something already existing, not re-submit stuff.

Suggested steps

So I'd basically suggest the following:

  1. Remove @RestResource(exported = false) from the Book's authors property.
  2. Submit the first request as you already have.
  3. Follow the link provided in the Location header you get returned by the server.
  4. Follow the authors link provided in the link target.
  5. Use the links returned by that resource and use them in subsequent creation requests to populate the authors property with.

If it's okay for you to create the author in separate steps I'd even advise to do so, as they return the URIs of the created authors immediately so that they can then be used in the subsequent creation of books.

like image 75
Oliver Drotbohm Avatar answered Oct 23 '22 07:10

Oliver Drotbohm