10

Currently we have an issue (a well known one) with Spring Data JPA + Spring Data REST (Hibernate as JPA implementation) when trying to update the collection (relation) which is a not the owning side.

The mapping is the following:

@Entity(name = Product.NAME)
public class Product {
...
@OneToMany(mappedBy = "baseProduct", fetch = FetchType.LAZY, targetEntity = Variant.class)
List<Variant> getVariants() {
... 

and on the other variant side:

@Entity(name = Variant.NAME)
public class Variant extends Product {
...
@ManyToOne(fetch = FetchType.LAZY, targetEntity = Product.class)
@JoinColumn(name = "baseproduct_id", referencedColumnName = "id")
Product getBaseProduct() {
...
}

all is good on the Java side if you use Spring Data JPA only, however if you want to update the "product" by updating its collection of variants and send PATCH request to https://localhost:8112/storefront/rest/product/21394435410197232 containing the payload of the new collection only (having 2 out of the 3 items):

{"variants":["22801810293768080","22801810293768096"]}

I get no exceptions or anything but since the owning side is the other side nothing is persisted and I got the old 3 items again.

I know that I can fix this by setting

@JoinColumn(name = "baseproduct_id", referencedColumnName = "id")

on both sides and not use mappedBy anywhere, however I have heard there is a performance implication which I am not sure how big it is (we got 100+ entities having @OneToMany) and I wonder is there better workaround via @PreUpdate listener or something ?

Vlad Mihalcea
  • 142,745
  • 71
  • 566
  • 911
jNayden
  • 1,592
  • 2
  • 17
  • 29

1 Answers1

7

You have to synchronize both sides of the bidirectional association, and also add on orphanRemoval and Cascade.

So, your mapping becomes:

@OneToMany(
    mappedBy = "baseProduct", 
    fetch = FetchType.LAZY, 
    targetEntity = Variant.class
    cascade = CascadeType.ALL, 
    orphanRemoval = true)
List<Variant> getVariants() {

And the two add/remove methods:

public void addVariant(Variant variant) {
    getVariants().add(variant);
    variant.setBaseProuct(this);
}

public void removeVariant(Variant variant) {
    variant.setBaseProuct(null);
    this.getVariants().remove(variant);
}

You need to implement equals and hashCode methods in the Variant child entity for the add and remove methods to work effectively.

Vlad Mihalcea
  • 142,745
  • 71
  • 566
  • 911
  • 1
    Thanks, however currently I don't want to remove the orphans I just want to remove the "connection" not removing any of the side :) Also I am not a huge fan of cascade(s) and maybe I am missing something but the addXXX and removeXXX methods will be invoked by Spring Data Rest somehow ? – jNayden Jan 20 '17 at 12:24
  • 1
    Then you have the wrong mapping because you haven't specified a `@JoinTable`. If you have a join table, then the best way to map it is to map the join table as an intermediate entity which has 2 `@ManyToOne `associations. I have an example in my book, [High-Performance Java Persistence](https://leanpub.com/high-performance-java-persistence). – Vlad Mihalcea Jan 20 '17 at 12:32
  • @VladMihalcea WIth this solution I get java.util.ConcurrentModificationException: null. I have a list of elements for which I have to invoke removeVariant method for every list item... How to solve this problem? I tried with iterator.remove() but so far without success:( – Matley Feb 01 '21 at 20:46