Search code examples
jpamany-to-manyeclipselink

JPA ManyToMany cascade with relation (ordering) table


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

  1. if I add B to A, A correctly has B in bList but B does not have A in aList
  2. upon B deletion, the R entities are properly deleted but the impacted A entities still have B in their aList

What I have tried

  1. Just to have a look, I tried to add some cascade attribute to the slave attribute, it was a bad idea
  2. I wanted to decorate CRUD methods for the EJB responsible of B entities: upon deletion, find all impacted A entities and remove the relationship in bList ==> failed because of so-far > 1): B cannot identify which A entities it is related to
  3. upon create and update operation of A, also update B entities by adding the R entity in their aList and proceed to an entityManager.merge() operation => JPA error because R already exists (even if I don't have a cascade merge attribute?)
  4. fetch a fresh copy of B before B's deletion but same as 2), the B entity does not have an up-to-date aList and cannot identity which A entities it is related to

What did I miss so far to have the relationship properly configured?

EDIT: Solution (draft)

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:

  1. In R's design (database & Entity definition), there is an unicity constraint between master and slave
  2. As there are multiple R entities, generic types are used. However, as in JPA interfaces with generic type are not allowed, I cannot call a.getBlist() from a R entity.

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);

        }

    }
    // ...
}

Additional note

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


Solution

  • 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.