Working with Spring Data REST, if you have a OneToMany
or ManyToOne
relationship, the PUT operation returns 200 on the "non-owning" entity but does not actually persist the joined resource.
Example Entities:
@Entity(name = 'author')
@ToString
class AuthorEntity implements Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id
String fullName
@ManyToMany(mappedBy = 'authors')
Set<BookEntity> books
}
@Entity(name = 'book')
@EqualsAndHashCode
class BookEntity implements Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id
@Column(nullable = false)
String title
@Column(nullable = false)
String isbn
@Column(nullable = false)
String publisher
@ManyToMany(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
Set<AuthorEntity> authors
}
If you back them with a PagingAndSortingRepository
, you can GET a Book
, follow the authors
link on the book and do a PUT with the URI of a author to associate with. You cannot go the other way.
If you do a GET on an Author and do a PUT on its books
link, the response returns 200, but the relationship is never persisted.
Is this the expected behavior?
The key to that is not so much anything in Spring Data REST - as you can easily get it to work in your scenario - but making sure that your model keeps both ends of the association in sync.
The problem you see here arises from the fact that Spring Data REST basically modifies the books
property of your AuthorEntity
. That itself doesn't reflect this update in the authors
property of the BookEntity
. This has to be worked around manually, which is not a constraint that Spring Data REST makes up but the way that JPA works in general. You will be able to reproduce the erroneous behavior by simply invoking setters manually and trying to persist the result.
If removing the bi-directional association is not an option (see below on why I'd recommend this) the only way to make this work is to make sure changes to the association are reflected on both sides. Usually people take care of this by manually adding the author to the BookEntity
when a book is added:
class AuthorEntity {
void add(BookEntity book) {
this.books.add(book);
if (!book.getAuthors().contains(this)) {
book.add(this);
}
}
}
The additional if clause would've to be added on the BookEntity
side as well if you want to make sure that changes from the other side are propagated, too. The if
is basically required as otherwise the two methods would constantly call themselves.
Spring Data REST, by default uses field access so that theres actually no method that you can put this logic into. One option would be to switch to property access and put the logic into the setters. Another option is to use a method annotated with @PreUpdate
/@PrePersist
that iterates over the entities and makes sure the modifications are reflected on both sides.
As you can see, this adds quite a lot of complexity to the domain model. As I joked on Twitter yesterday:
#1 rule of bi-directional associations: don't use them… :)
It usually simplifies the matter if you try not to use bi-directional relationship whenever possible and rather fall back to a repository to obtain all the entities that make up the backside of the association.
A good heuristics to determine which side to cut is to think about which side of the association is really core and crucial to the domain you're modeling. In your case I'd argue that it's perfectly fine for an author to exist with no books written by her. On the flip side, a book without an author doesn't make too much sense at all. So I'd keep the authors
property in BookEntity
but introduce the following method on the BookRepository
:
interface BookRepository extends Repository<Book, Long> {
List<Book> findByAuthor(Author author);
}
Yes, that requires all clients that previously could just have invoked author.getBooks()
to now work with a repository. But on the positive side you've removed all the cruft from your domain objects and created a clear dependency direction from book to author along the way. Books depend on authors but not the other way round.