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 实体在数据库中正确持久化和删除
- 如果我将 B 添加到 A,A 正确地在
bList
中包含 B,但 B 在 aList
中没有 A
- 删除 B 后,R 实体被正确删除,但受影响的 A 实体在其
aList
中仍然有 B
我试过的
- 只是为了看看,我尝试在
slave
属性中添加一些级联属性,这是个坏主意
- 我想为负责 B 实体的 EJB 修饰 CRUD 方法:删除后,找到所有受影响的 A 实体并删除
bList
中的关系 ==> 由于 so-far > 1)
而失败:B无法识别它与哪些 A 实体相关
- 在 A 的创建和更新操作中,还通过在
aList
中添加 R 实体来更新 B 实体,并继续进行 entityManager.merge()
操作 => JPA 错误,因为 R 已经存在(即使我没有级联合并属性?)
- 在删除 B 之前获取 B 的新副本,但与 2) 相同,B 实体没有最新的
aList
并且无法识别它与哪个 A 实体相关
到目前为止我错过了什么来正确配置关系?
编辑:解决方案(草稿)
我附加解决方案以连接评论和答案的反馈。正如 Chris 正确提到的那样,它是关于 JPA 事务范围的。在继续之前,一些细节:
- 在R的设计中(数据库&实体定义),master和slave之间存在唯一性约束
- 由于存在多个 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);
}
}
// ...
}
补充说明
到目前为止,bList
和 aList
是最新的。如果一个 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 的更改合并回持久性上下文。
数据设计
我的 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 实体在数据库中正确持久化和删除
- 如果我将 B 添加到 A,A 正确地在
bList
中包含 B,但 B 在aList
中没有 A
- 删除 B 后,R 实体被正确删除,但受影响的 A 实体在其
aList
中仍然有 B
我试过的
- 只是为了看看,我尝试在
slave
属性中添加一些级联属性,这是个坏主意 - 我想为负责 B 实体的 EJB 修饰 CRUD 方法:删除后,找到所有受影响的 A 实体并删除
bList
中的关系 ==> 由于so-far > 1)
而失败:B无法识别它与哪些 A 实体相关 - 在 A 的创建和更新操作中,还通过在
aList
中添加 R 实体来更新 B 实体,并继续进行entityManager.merge()
操作 => JPA 错误,因为 R 已经存在(即使我没有级联合并属性?) - 在删除 B 之前获取 B 的新副本,但与 2) 相同,B 实体没有最新的
aList
并且无法识别它与哪个 A 实体相关
到目前为止我错过了什么来正确配置关系?
编辑:解决方案(草稿)
我附加解决方案以连接评论和答案的反馈。正如 Chris 正确提到的那样,它是关于 JPA 事务范围的。在继续之前,一些细节:
- 在R的设计中(数据库&实体定义),master和slave之间存在唯一性约束
- 由于存在多个 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);
}
}
// ...
}
补充说明
到目前为止,bList
和 aList
是最新的。如果一个 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 的更改合并回持久性上下文。