JPA:关于 OneToMany 关系中阻抗不匹配的问题

Posted

技术标签:

【中文标题】JPA:关于 OneToMany 关系中阻抗不匹配的问题【英文标题】:JPA: question on impedance mismatch in OneToMany relations 【发布时间】:2010-09-02 19:20:43 【问题描述】:

我有一个关于 JPA-2.0(提供者是 Hibernate)关系及其在 Java 中的相应管理的问题。假设我有一个部门和一个员工实体:

@Entity
public class Department 
  ...
  @OneToMany(mappedBy = "department")
  private Set<Employee> employees = new HashSet<Employee>();
  ...


@Entity
public class Employee 
  ...
  @ManyToOne(targetEntity = Department.class)
  @JoinColumn
  private Department department;
  ...

现在我知道我必须自己管理 Java 关系,如以下单元测试:

@Transactional
@Test
public void testBoth() 
  Department d = new Department();
  Employee e = new Employee();
  e.setDepartment(d);
  d.getEmployees().add(e);
  em.persist(d);
  em.persist(e);
  assertNotNull(em.find(Employee.class, e.getId()).getDepartment());
  assertNotNull(em.find(Department.class, d.getId()).getEmployees());

如果我遗漏了e.setDepartment(d)d.getEmployees().add(e),则断言将失败。到现在为止还挺好。如果我在两者之间提交数据库事务怎么办?

@Test
public void testBoth() 
  EntityManager em = emf.createEntityManager();
  em.getTransaction().begin();
  Department d = new Department();
  Employee e = new Employee();
  e.setDepartment(d);
  d.getEmployees().add(e);
  em.persist(d);
  em.persist(e);
  em.getTransaction().commit();
  em.close();
  em = emf.createEntityManager();
  em.getTransaction().begin();
  assertNotNull(em.find(Employee.class, e.getId()).getDepartment());
  assertNotNull(em.find(Department.class, d.getId()).getEmployees());
  em.getTransaction().commit();
  em.close();

我还需要管理关系的双方吗?不,事实证明,我不必这样做。有了这个修改

e.setDepartment(d);
//d.getEmployees().add(e);

断言仍然成功。但是,如果我只设置另一边:

//e.setDepartment(d);
d.getEmployees().add(e);

断言失败。为什么?是因为 Employee 是关系的所有者吗?我可以通过不同的注释来改变这种行为吗?还是只是“OneToMany”的“One”一侧决定何时填充数据库中的外键字段?

【问题讨论】:

您正在通过em.close(); em = emf.createEntityManager(); 创建一个新的EM,这对事情有很大影响。尝试在不创建新 EM 的情况下使用事务。 【参考方案1】:

我不知道您的测试试图证明什么,但事实是您在使用双向关联时必须处理关联的双方。不这样做是不正确的。时期。

更新: 虽然 axtavt 提到的规范参考当然是准确的,但我坚持认为,您绝对必须设置双向关联的双方。不这样做是不正确的,并且您在第一个持久性上下文中的实体之间的关联会损坏。 JPA wiki book 是这样写的:

与所有双向关系一样,维护双向关系是您的对象模型和应用程序的责任。 JPA 中没有魔法,如果您在集合的一侧添加或删除,您还必须从另一侧添加或删除,请参阅object corruption。从技术上讲,如果您仅从关系的拥有方添加/删除,数据库将正确更新,但您的对象模型将不同步,这可能会导致问题。

换句话说,在 Java 中管理双向关联的唯一正确安全方法是设置链接的两侧。这通常使用防御性链接管理方法来完成,如下所示:

@Entity
public class Department 
    ...
    @OneToMany(mappedBy = "department")
    private Set<Employee> employees = new HashSet<Employee>();
    ...

    public void addToEmployees(Employee employee) 
        this.employees.add(employee);
        employee.setDepartment(this);
    

我再说一遍,不这样做是不正确的。您的测试之所以有效,是因为您在新的持久性上下文中访问数据库(即非常特殊的情况,而不是一般情况),但代码会在许多其他情况下中断。

【讨论】:

我并不是在建议适当的关系管理的捷径,我只是对观察到的行为有点困惑。所以我会确保 Java 方始终管理双方的关系,谢谢。 @wallenborn 对不起,我当时不知何故误解了这个问题。【参考方案2】:

JPA 中的实体关系有拥有和反面。数据库更新由拥有方的状态决定。在您的情况下,由于 mappedBy 属性,Employee 是拥有方。

来自JPA 2.0 specification:

2.9 实体关系

...

关系可能是双向的,也可能是 单向。一个双向 关系都有拥有的一面 和一个反向(非拥有)的一面。一种 单向关系只有 拥有方。 a的拥有方 关系决定更新 数据库中的关系,如 节中描述 3.2.4。

以下规则适用于双向关系:

双向的反面 关系必须指其拥有 同时使用 mappedBy 元素 一对一、一对多或多对多 注解。 mappedBy 元素 指定属性或字段 作为所有者的实体 关系。 多方面 一对多/多对一 双向关系必须是 拥有方,因此是 mappedBy 元素不能在 多对一注释。 为 一对一双向 关系,拥有方 对应于包含的一侧 对应的外键。 为 多对多双向 任何一方的关系都可能是 拥有方。

【讨论】:

谢谢你,这回答了我的问题。【参考方案3】:

如果您仅在先前的上下文中更新拥有方,那么在新的持久性上下文中的第二次测试成功的原因是持久性提供者显然无法知道在持久化时您没有更新反面。出于持久性目的,它只关心拥有方。但是,当您从持久性提供程序获取持久性对象时,提供程序会在两侧正确设置双向关联(只是假设它们也被正确持久化)。但是,正如这里的许多其他人已经指出的那样,持久性提供者不负责完成新创建的双向关联,您应该始终在代码中正确维护双向关联。

【讨论】:

是的,数据库只能存储一条信息,当从数据库中检索到对象时,JPA会重建双方。

以上是关于JPA:关于 OneToMany 关系中阻抗不匹配的问题的主要内容,如果未能解决你的问题,请参考以下文章

如何在 JPA 中定义单向 OneToMany 关系

Hibernate/JPA:获取 OneToMany 关系的一个(第一个)元素

JPA @OneToMany 不保存父 ID

通过 JPA 查询

子表在 JPA 中的 OneToMany 关系中没有映射

如何在 @OneToMany 关系映射的列上使用 JPA findBy 查询?