How to maintain bi-directional relationships with Spring Data REST and JPA?

16,962

Solution 1

tl;dr

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

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.

How to solve this?

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.

Removing the root cause of the issue

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.

Solution 2

I faced a similar problem, while sending my POJO(containing bi-directional mapping @OneToMany and @ManyToOne) as JSON via REST api, the data was persisted in both the parent and child entities but the foreign key relation was not established. This happens because bidirectional associations need to be manually maintained.

JPA provides an annotation @PrePersist which can be used to make sure that the method annotated with it is executed before the entity is persisted. Since, JPA first inserts the parent entity to the database followed by the child entity, I included a method annotated with @PrePersist which would iterate through the list of child entities and manually set the parent entity to it.

In your case it would be something like this:

class AuthorEntitiy {
    @PrePersist
    public void populateBooks {
        for(BookEntity book : books)
            book.addToAuthorList(this);   
    }
}

class BookEntity {
    @PrePersist
    public void populateAuthors {
        for(AuthorEntity author : authors)
            author.addToBookList(this);   
    }
}

After this you might get an infinite recursion error, to avoid that annotate your parent class with @JsonManagedReference and your child class with @JsonBackReference. This solution worked for me, hopefully it will work for you too.

Share:
16,962
keaplogik
Author by

keaplogik

http://keaplogik.blogspot.com

Updated on June 05, 2022

Comments

  • keaplogik
    keaplogik almost 2 years

    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?

  • geoand
    geoand almost 9 years
    The "template" of your answers should be standardized somehow on SO :P! Good stuff!
  • JR Utily
    JR Utily almost 9 years
    If I read you well, you are suggesting to remove all OneToMany relations and to keep only their ManyToOne pendants ? This would be a real big change in the way I model Entities...
  • norgence
    norgence almost 9 years
    No. I don't recommend anything on a by-annotation basis. I recommend not to use bi-directional associations. In fact, I didn't even mention any of the annotations, so it's not entirely obvious to me how you derive that interpretation. Which side of the association you cut is highly depending on the use-case. Please re-read the section mentioning the heuristics for justification.
  • JR Utily
    JR Utily almost 9 years
    You are right, I was taking a shortcut. As on an existing bi-directional One to many / Many to one relationship, the Many to one will be almost always the owner side of the relationship, I thought it would be much less effort to cut the One to many part. If we want to keep the One to many side and cut the other half we should redo all the mapping (through a join table for example). But you are right, there are some cases where the One to many will be much more interesting for the business logic. Thanks Oliver!
  • JanneK
    JanneK over 8 years
    Just to add a reference to JPA spec, on the page 42 (chapter 2.9) of the PDF version of "JSR-000317 Java(tm) Persistence 2.0" specification it is stated that: "Note that it is the application that bears responsibility for maintaining the consistency of runtime relationships—for example, for insuring that the “one” and the “many” sides of a bidirectional relationship are consistent with one another when the application updates the relationship at runtime."
  • Alan Hay
    Alan Hay over 7 years
    @OliverGierke "One option would be to switch to property access and put the logic into the setters". How do I tell SDR to use property access?
  • drenda
    drenda over 6 years
    @OliverGierke interesting reply. I am not entirely convinced about the bidirectional association. Let's think about a Ticket having a List<Payment>. We can argue a ticket can exist also without a Payment, instead a Payment without a related Ticket doesn't make much sense. So I would keep ticket property inside Payment. But in this way how you can create bean validation rules like a simple "the sum of payments should not be > ticket price"? Would you put that on Payment? Is this a good practice? Thanks
  • guneetgstar
    guneetgstar over 3 years
    Facing the same problem but @PrePersist not working for me as it the relation field returns null inside @PrePersist callback and no child/parent set inside it.