原文: https://howtodoinjava.com/hibernate/how-to-define-association-mappings-between-hibernate-entities/

在上一教程中,我们了解了 Hiberate 实体的四种持久状态,并且我们注意到,只有在使用某些注解对 Java 对象进行注解时,hibernate 才能将其标识为持久对象。 否则,将它们视为与数据库没有直接关系的简单普通 Java 对象。 继续,当我们用 JPA 注解对 Java 类进行注解并使它们成为持久化实体时,我们可能会遇到两个实体可以关联并且必须相互引用的情况,无论是单向还是双向。 在实际在 Hiberate 实体之间创建引用之前,让我们了解一些基本知识。

了解实体及其关联

实体可以包含对其他实体的引用,可以直接作为嵌入式属性或字段,也可以通过某种集合(数组,集合,列表等)间接包含对其他实体的引用。 这些关联使用基础表中的外键关系表示。 这些外键将依赖于参与表使用的标识符。

当一对实体中只有一个包含对另一个实体的引用时,关联为单向。 如果关联是相互的,则称为双向

初学者在设计实体模型时的一个常见错误是试图使所有关联都是双向的。 请记住,不是对象模型的自然组成部分的关联不应被强加到其中。 Hibernate 查询语言(HQL)通常被证明是在需要时访问所需信息的更自然的方法。

理想情况下,我们希望指示仅对关系一端的更改会导致外键的任何更新。 实际上,Hibernate 允许我们通过将关联的一端标记为由另一端管理来做到这一点。

在 Hiberate 映射关联中,一个参与类(只有一个)“管理关系”,而另一类则使用“mappedBy”属性“被管理”。 我们不应该使关联的两端都“管理关系”。 永远不要做。

注意,“mappedBy”纯粹是关于实体之间的外键关系如何保存的。 它与使用级联功能保存实体本身无关。

尽管 Hibernate 让我们指定对一个关联的更改将导致对数据库的更改,但是它不允许我们使对关联的一端的更改自动反映在 Java POJO 的另一端。 为此,我们必须使用级联功能。

通过示例了解关联

让我们快速构建示例,以了解我们上面已阅读的有关实体关联的内容以及应如何完成。 在此示例中,我使用两个简单实体(AccountEntityEmployeeEntity),然后在它们之间创建 一对一关联。 然后,我们将看到各种关联选项如何改变运行时行为和复杂性。

AccountEntity.java

  1. @Entity
  2. @Table(name = "Account")
  3. public class AccountEntity implements Serializable
  4. {
  5. private static final long serialVersionUID = 1L;
  6. @Id
  7. @Column(name = "ID", unique = true, nullable = false)
  8. @GeneratedValue(strategy = GenerationType.SEQUENCE)
  9. private Integer accountId;
  10. @Column(name = "ACC_NO", unique = false, nullable = false, length = 100)
  11. private String accountNumber;
  12. //We will define the association here
  13. EmployeeEntity employee;
  14. //Getters and Setters are not shown for brevity
  15. }

EmployeeEntity.java

  1. @Entity
  2. @Table(name = "Employee")
  3. public class EmployeeEntity implements Serializable
  4. {
  5. private static final long serialVersionUID = -1798070786993154676L;
  6. @Id
  7. @Column(name = "ID", unique = true, nullable = false)
  8. @GeneratedValue(strategy = GenerationType.SEQUENCE)
  9. private Integer employeeId;
  10. @Column(name = "FIRST_NAME", unique = false, nullable = false, length = 100)
  11. private String firstName;
  12. @Column(name = "LAST_NAME", unique = false, nullable = false, length = 100)
  13. private String lastName;
  14. //We will define the association here
  15. AccountEntity account;
  16. //Getters and Setters are not shown for brevity
  17. }

方案 1)由两个实体管理的关联

在这种类型的关联中,我们将使用@OneToOne注解如下定义关联。

  1. //Inside EmployeeEntity.java
  2. @OneToOne
  3. AccountEntity account;
  4. //Inside AccountEntity.java
  5. @OneToOne
  6. EmployeeEntity employee;

通过上述关联,两端都在管理关联,因此必须使用实体 java 文件中定义的设置器方法使用彼此的信息来更新双方。 如果您未这样做,则将无法获取关联的实体信息,并且该信息将作为null返回。

  1. public class TestHibernate
  2. {
  3. public static void main(String[] args)
  4. {
  5. Session sessionOne = HibernateUtil.getSessionFactory().openSession();
  6. sessionOne.beginTransaction();
  7. // Create new Employee object
  8. EmployeeEntity emp = new EmployeeEntity();
  9. emp.setFirstName("Lokesh");
  10. emp.setLastName("Gupta");
  11. // Create new Employee object
  12. AccountEntity acc = new AccountEntity();
  13. acc.setAccountNumber("DUMMY_ACCOUNT");
  14. emp.setAccount(acc);
  15. //acc.setEmployee(emp);
  16. sessionOne.save(acc);
  17. sessionOne.save(emp);
  18. sessionOne.getTransaction().commit();
  19. Integer genEmpId = emp.getEmployeeId();
  20. Integer genAccId = acc.getAccountId();
  21. Session sessionTwo = HibernateUtil.getSessionFactory().openSession();
  22. sessionTwo.beginTransaction();
  23. EmployeeEntity employee = (EmployeeEntity) sessionTwo.get(EmployeeEntity.class, genEmpId);
  24. AccountEntity account = (AccountEntity) sessionTwo.get(AccountEntity.class, genAccId);
  25. System.out.println(employee.getEmployeeId());
  26. System.out.println(employee.getAccount().getAccountNumber());
  27. System.out.println(account.getAccountId());
  28. System.out.println(account.getEmployee().getEmployeeId());
  29. HibernateUtil.shutdown();
  30. }
  31. }
  32. Output:
  33. Hibernate: insert into Account (ACC_NO, employee_ID, ID) values (?, ?, ?)
  34. Hibernate: insert into Employee (account_ID, FIRST_NAME, LAST_NAME, ID) values (?, ?, ?, ?)
  35. Hibernate: select employeeen0_.ID as ID1_1_0_, employeeen0_.account_ID as account_4_1_0_, employeeen0_.FIRST_NAME as FIRST_NA2_1_0_,
  36. employeeen0_.LAST_NAME as LAST_NAM3_1_0_, accountent1_.ID as ID1_0_1_, accountent1_.ACC_NO as ACC_NO2_0_1_,
  37. accountent1_.employee_ID as employee3_0_1_, employeeen2_.ID as ID1_1_2_, employeeen2_.account_ID as account_4_1_2_,
  38. employeeen2_.FIRST_NAME as FIRST_NA2_1_2_, employeeen2_.LAST_NAME as LAST_NAM3_1_2_ from Employee
  39. employeeen0_ left outer join Account accountent1_ on employeeen0_.account_ID=accountent1_.ID
  40. left outer join Employee employeeen2_ on accountent1_.employee_ID=employeeen2_.ID where employeeen0_.ID=?
  41. 20
  42. DUMMY_ACCOUNT
  43. 10
  44. Exception in thread "main" java.lang.NullPointerException
  45. at com.howtodoinjava.test.TestHibernate.main(TestHibernate.java:43)

您会看到我已经在客户实体中设置了帐户实体,所以我能够得到它。 但是我注解了“acc.setEmployee(emp);”行,因此未在帐户实体内部设置雇员实体,因此我无法获取它。

还要注意上面的插入查询。 这两个表都具有与列名employee_IDaccount_ID对应的外键关联。 它是数据中循环依赖项的示例,可以随时轻松关闭您的应用。

要正确存储上述关系,必须取消注释“acc.setEmployee(emp);”行。 取消注释该行后,您将能够根据需要存储和检索关联,但是仍然存在循环依赖项。

取消注释该行后,输出将如下所示:

  1. Hibernate: insert into Account (ACC_NO, employee_ID, ID) values (?, ?, ?)
  2. Hibernate: insert into Employee (account_ID, FIRST_NAME, LAST_NAME, ID) values (?, ?, ?, ?)
  3. Hibernate: update Account set ACC_NO=?, employee_ID=? where ID=?
  4. Hibernate: select employeeen0_.ID as ID1_1_0_, employeeen0_.account_ID as account_4_1_0_, employeeen0_.FIRST_NAME as FIRST_NA2_1_0_,
  5. employeeen0_.LAST_NAME as LAST_NAM3_1_0_, accountent1_.ID as ID1_0_1_, accountent1_.ACC_NO as ACC_NO2_0_1_,
  6. accountent1_.employee_ID as employee3_0_1_, employeeen2_.ID as ID1_1_2_, employeeen2_.account_ID as account_4_1_2_,
  7. employeeen2_.FIRST_NAME as FIRST_NA2_1_2_, employeeen2_.LAST_NAME as LAST_NAM3_1_2_ from Employee
  8. employeeen0_ left outer join Account accountent1_ on employeeen0_.account_ID=accountent1_.ID
  9. left outer join Employee employeeen2_ on accountent1_.employee_ID=employeeen2_.ID where employeeen0_.ID=?
  10. 20
  11. DUMMY_ACCOUNT
  12. 10
  13. 20

方案 2)由单个实体管理的关联

假设关联是由EmployeeEntity管理的,那么两个实体中的注解将如下所示。

  1. //Inside EmployeeEntity.java
  2. @OneToOne
  3. AccountEntity account;
  4. //Inside AccountEntity.java
  5. @OneToOne (mappedBy = "account")
  6. EmployeeEntity employee;

现在告诉 Hiberate ,该关联是由EmployeeEntity管理的,我们将在AccountEntity中添加mappingBy属性以使其易于管理。 现在,要测试上述代码,我们将只需要使用“emp.setAccount(acc);”设置一次关联,并且由员工实体来管理该关系。 AccountEntity不需要明确地知道任何事情。

让我们看看下面的测试运行。

  1. Session sessionOne = HibernateUtil.getSessionFactory().openSession();
  2. sessionOne.beginTransaction();
  3. // Create new Employee object
  4. EmployeeEntity emp = new EmployeeEntity();
  5. emp.setFirstName("Lokesh");
  6. emp.setLastName("Gupta");
  7. // Create new Employee object
  8. AccountEntity acc = new AccountEntity();
  9. acc.setAccountNumber("DUMMY_ACCOUNT");
  10. emp.setAccount(acc);
  11. //acc.setEmployee(emp);
  12. sessionOne.save(acc);
  13. sessionOne.save(emp);
  14. sessionOne.getTransaction().commit();
  15. Integer genEmpId = emp.getEmployeeId();
  16. Integer genAccId = acc.getAccountId();
  17. Session sessionTwo = HibernateUtil.getSessionFactory().openSession();
  18. sessionTwo.beginTransaction();
  19. EmployeeEntity employee = (EmployeeEntity) sessionTwo.get(EmployeeEntity.class, genEmpId);
  20. AccountEntity account = (AccountEntity) sessionTwo.get(AccountEntity.class, genAccId);
  21. System.out.println(employee.getEmployeeId());
  22. System.out.println(employee.getAccount().getAccountNumber());
  23. System.out.println(account.getAccountId());
  24. System.out.println(account.getEmployee().getEmployeeId());
  25. HibernateUtil.shutdown();
  26. Output:
  27. Hibernate: insert into Account (ACC_NO, ID) values (?, ?)
  28. Hibernate: insert into Employee (account_ID, FIRST_NAME, LAST_NAME, ID) values (?, ?, ?, ?)
  29. Hibernate: select employeeen0_.ID as ID1_1_0_, employeeen0_.account_ID as account_4_1_0_, employeeen0_.FIRST_NAME as FIRST_NA2_1_0_,
  30. employeeen0_.LAST_NAME as LAST_NAM3_1_0_, accountent1_.ID as ID1_0_1_, accountent1_.ACC_NO as ACC_NO2_0_1_ from Employee employeeen0_
  31. left outer join Account accountent1_ on employeeen0_.account_ID=accountent1_.ID where employeeen0_.ID=?
  32. Hibernate: select employeeen0_.ID as ID1_1_1_, employeeen0_.account_ID as account_4_1_1_, employeeen0_.FIRST_NAME as FIRST_NA2_1_1_,
  33. employeeen0_.LAST_NAME as LAST_NAM3_1_1_, accountent1_.ID as ID1_0_0_, accountent1_.ACC_NO as ACC_NO2_0_0_ from Employee employeeen0_
  34. left outer join Account accountent1_ on employeeen0_.account_ID=accountent1_.ID where employeeen0_.account_ID=?
  35. 20
  36. DUMMY_ACCOUNT
  37. 10
  38. 20

您会看到您不需要告诉帐户实体任何内容(“acc.setEmployee(emp)”已注解)。 员工实体通过两种方式管理关联。

另一个观察结果是关于外键列,我们现在只有一个外键列,即account_IDEmployee表。 因此也没有循环依赖。 都好。

定义各种映射关联的指南

上面的示例显示了如何在一对一映射中管理实体之间的关联。 在上面的示例中,我们也可以选择由AccountEntity管理的关联,并且只需进行较小的代码更改即可完成所有工作。 但是,在其他映射的情况下(例如,一对多或多对一),您将无法随意定义关联。 您需要规则。

下表显示了如何选择关系的一方,该一方应成为双向关联的所有者。 请记住,要使关联成为所有者,您必须将另一端标记为被另一端映射

关联类型 选项/用法
一对一 可以将任一端作为所有者,但应其中之一(只有一个); 如果您未指定此选项,则最终会产生循环依赖项。
一对多 多的一端必须成为联合的所有者。
多对一 这与从相反的角度看的一对多关系相同,因此适用相同的规则:必须使多的一端成为关联的所有者。
多对多 关联的任何一端都可以成为所有者。

如果这一切看起来都令人困惑,请记住关联所有权仅与数据库中外键的管理有关。 我建议您阅读之前的教程,讨论“一对一映射”,“一对多映射”和“多对多映射”的详细信息。 他们将帮助您建立更强大的概念。

如果您有任何问题,请在下面给我留言。

祝您学习愉快!