Data design
My A entity <-> B entity relationship is done via a R entity. However, R has additional information which prevents the use of @ManyToMany:
id | id_a | id_b | previous | ordering_index
----------------------------------------------
1 | 1 | 10 | 2 | 1
2 | 1 | 15 | null | 0
3 | 1 | 18 | 1 | 2
JPA entities
@Entity
// ...
public class A{
// ...
@OneToMany(mappedBy="master", cascade = Cascade.ALL, orphanRemoval = true)
@OrderBy("ordering")
private List<R> bList;
// ....
}
@Entity
// ...
public class B{
// ...
@OneToMany(mappedBy="slave", cascade = Cascade.REMOVE)
private List<R> aList;
// ....
}
@Entity
// ...
public class R{
@Id
// ...
private long id;
@ManyToOne
@JoinColumn(name = "id_a", referencedColumnName = "id")
private A master;
@ManyToOne
@JoinColumn(name = "id_b", referencedColumnName = "id")
private B slave;
@OneToOne
@JoinColumn(name = "previous", referencedColumnName = "id")
private R previous;
@Column(name = "ordering_index")
private int ordering;
}
The relationship is not bidirectional: the user interface allows adding B entities to an A entity but editing a B entity does not allow editing the aList
.
So far: the problem
upon A creation, update and deletion, the R entities are properly persisted and deleted in the database
bList
but B does not have A in aList
aList
What I have tried
slave
attribute, it was a bad ideabList
==> failed because of so-far > 1)
: B cannot identify which A entities it is related toaList
and proceed to an entityManager.merge()
operation => JPA error because R already exists (even if I don't have a cascade merge attribute?)aList
and cannot identity which A entities it is related toWhat did I miss so far to have the relationship properly configured?
I append the solution to concatenate the feedback of comments and answers. As Chris correctly mentioned, it was about the JPA transaction scope. Before going further, some detail:
My solution was to dig in my 3rd attempt (upon create and update operation of A, also update B entities) as suggested by Chris' answer:
public class aEjb{
// ...
public void update(A a){
// by A -> R cascade, all the bList will be updated
em.merge(a);
// BUT !!!
// as the transaction did not end, the newly created
// relationship do not have a primary key generated
// yet so if I do:
for(R r : a.getBList()){
B b = r.getSlave();
// I'm here adding r = (null, a, b)
b.add(r);
// as r has a null primary key, JPA will try to
// create it, even if I removed all cascade annotation
// on b side => I didn't really understand why though
bEjb.update(b);
// as there is UNIQUE(a,b) constraint, JPA will
// throw an exception
}
// the solution was to fetch a fresh copy of the
// cascaded created R entity. As the primary key
// is not known yet, I have to go through sth like
for(R r : a.getBlist()){
B b = r.getSlave();
R updatedR = rEjb.findByMasterAndSlave(a, b);
// I'm here adding updatedR = (123, a, b)
b.add(updatedR)
// as updatedR primary key is not null, JPA
// won't force any cascade operation, and that's
// great becaues I don't want any cascade here
bEjb.update(b);
}
}
// ...
}
So far, bList
and aList
are up-to-date. If a B entity is deleted, I have an available aList
to loop through. However, one more problem occurred: what if I removed a B entity from a A entity but deleting only the link, not any of these entity?
The solution is to avoid the option orphanRemoval = true
:
Before:
@Entity
// ...
public class A{
// ...
@OneToMany(mappedBy="master", cascade = Cascade.ALL, orphanRemoval = true)
@OrderBy("ordering")
private List<R> bList;
// ....
}
After:
@Entity
// ...
public class A{
// ...
@OneToMany(mappedBy="master", cascade = Cascade.ALL)
@OrderBy("ordering")
private List<R> bList;
@Transient
private List<R> orphanBList;
// ....
public void addB(B b){
R r = new R(a,b);
bList.add(r);
}
public void removeR(R r){
bList.remove(r);
orphanBList.add(r);
}
}
Then I proceed to a similar EJB operation as defined to update the impacted B entities
The A->R and B->R are two separate bidirectional relationships that must be maintained to keep your object model in synch with what is in the database. When ever you add a new R instance, you need to associate both sides of both the A and B references, and since you are changing both A and B instances, you are responsible for merging these changes back in if they are detached. There are a number of ways to handle this, the simplest is something like:
em.getTransaction().begin();
A a = em.find(A.class, AsID);
B b = em.find(B.class, BsID);
R r = new R(a, b);
a.bList.add(r);
b.aList.add(r);
em.persist(r);
em.getTransaction().commit();
In the above code, the order isn't all that important as it is all done in the same context - A and B are still managed, so changes are automatically picked up. The persist call isn't even really needed, as you have the Cascade.ALL set on the A->R relationship but I find it better to call out explicitly. This will ensure that R is always in the list no matter if you are looking at bList or aList.
For deleting an A, you must do something of the form:
em.getTransaction().begin();
A a = em.find(A.class, AsID);
for (R r: a.bList) {
if (r.slave != null)
r.slave.aList.remove(r);
}
em.remove(a);
em.getTransaction().commit();
Again, this works because everything is managed in the same context. If you are passing this back and forth around through DAO type calls, you'll have to handle merging back detached entities yourself, specifically merging the changes to the r.slave back into the persistence context.