JPA ManyToMany 与关系级联(排序)table

JPA ManyToMany cascade with relation (ordering) table

数据设计

我的 A 实体 <-> B 实体关系是通过 R 实体完成的。但是,R 具有阻止使用 @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 实体

@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;
}

关系不是双向的:用户界面允许将 B 实体添加到 A 实体,但编辑 B 实体不允许编辑 aList

到目前为止:问题

在创建、更新和删除 A 后,R 实体在数据库中正确持久化和删除

  1. 如果我将 B 添加到 A,A 正确地在 bList 中包含 B,但 B 在 aList
  2. 中没有 A
  3. 删除 B 后,R 实体被正确删除,但受影响的 A 实体在其 aList
  4. 中仍然有 B

我试过的

  1. 只是为了看看,我尝试在 slave 属性中添加一些级联属性,这是个坏主意
  2. 我想为负责 B 实体的 EJB 修饰 CRUD 方法:删除后,找到所有受影响的 A 实体并删除 bList 中的关系 ==> 由于 so-far > 1) 而失败:B无法识别它与哪些 A 实体相关
  3. 在 A 的创建和更新操作中,还通过在 aList 中添加 R 实体来更新 B 实体,并继续进行 entityManager.merge() 操作 => JPA 错误,因为 R 已经存在(即使我没有级联合并属性?)
  4. 在删除 B 之前获取 B 的新副本,但与 2) 相同,B 实体没有最新的 aList 并且无法识别它与哪个 A 实体相关

到目前为止我错过了什么来正确配置关系?

编辑:解决方案(草稿)

我附加解决方案以连接评论和答案的反馈。正如 Chris 正确提到的那样,它是关于 JPA 事务范围的。在继续之前,一些细节:

  1. 在R的设计中(数据库&实体定义),master和slave之间存在唯一性约束
  2. 由于存在多个 R 实体,因此使用泛型。但是,由于在 JPA 接口中不允许使用泛型类型,因此我无法从 R 实体调用 a.getBlist()。

我的解决方案是在我的第三次尝试中挖掘(在 A 的创建和更新操作中,同时更新 B 实体),正如 Chris 的回答所建议的:

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

        }

    }
    // ...
}

补充说明

到目前为止,bListaList 是最新的。如果一个 B 实体被删除,我有一个可用的 aList 可以循环。但是,又出现了一个问题:如果我从 A 实体中删除了 B 实体,但只删除了 link,而不是这些实体中的任何一个,该怎么办?

解决方案是避免选项orphanRemoval = true:

之前:

@Entity
// ...
public class A{
    // ...
    @OneToMany(mappedBy="master", cascade = Cascade.ALL, orphanRemoval = true)
    @OrderBy("ordering")
    private List<R> bList;
    // ....
}

之后:

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

然后我继续执行定义的类似 EJB 操作以更新受影响的 B 实体

您需要添加这样的构造函数来解决问题 1:

public R(A master, B slave, R previous, int ordering){
    this.master = master;
    master.getBList().add(this);
    this.slave = slave;
    slave.getAList().add(this);
    this.previous = previous;
    this.ordering = ordering;
}

尝试按照同样的方法解决问题2。

A​​->R 和 B->R 是两个独立的双向关系,必须维护它们以使您的对象模型与数据库中的内容保持同步。当您添加一个新的 R 实例时,您需要关联 A 和 B 引用的两侧,并且由于您同时更改了 A 和 B 实例,因此如果它们是分离的,您有责任将这些更改合并回去。有很多方法可以解决这个问题,最简单的是:

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

在上面的代码中,顺序并不是那么重要,因为它们都是在相同的上下文中完成的 - A 和 B 仍然是受管理的,因此会自动获取更改。甚至不需要持久调用,因为您在 A->R 关系上设置了 Cascade.ALL,但我发现显式调用更好。这将确保无论您查看的是 bList 还是 aList,R 始终在列表中。

要删除 A,您必须执行以下形式的操作:

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

同样,这之所以有效,是因为一切都在同一上下文中进行管理。如果您通过 DAO 类型调用来回传递它,则您必须自己处理合并回分离的实体,特别是将对 r.slave 的更改合并回持久性上下文。