如何使用注解在 Hibernate 4 和 Spring 中定义不同类型的关系?
Posted
技术标签:
【中文标题】如何使用注解在 Hibernate 4 和 Spring 中定义不同类型的关系?【英文标题】:How do I use annotations to define different types of relationships in Hibernate 4 and Spring? 【发布时间】:2014-08-07 02:09:58 【问题描述】:我有两个类,Foo
和Bar
,如下:
public class Foo
private Long fooId;
private Bar bar;
//Yes, this doesn't actually make any sense,
//having both a list and a single object here, its an example.
private List<Bar> bars;
public class Bar
private Long barId;
private Foo foo;
如何使用 Hibernate 4 的注解为这些类实现(单向/双向)一对多、多对一或多对多关系?
此外,我如何配置我的一对多以进行孤儿删除、延迟加载以及在处理集合时导致LazyInitialiaizationException
的原因以及如何解决问题?
【问题讨论】:
没错,直到 20k,这是有道理的。这个“问题”可能会被删除,但很可能会被关闭。祝你好运,谢谢你的努力。 wikipedia/wikibooks 实际上是这个地方,并且已经有关于这种 JPA 关系的书籍,所以为什么不只是“改进”那些如果不完整的话 我确实考虑到了这一点,然后我考虑了可能受益的人在那里遇到它的可能性有多大。我主要想要一个 SO 资源,我可以在回答人们的问题时指出,因为我厌倦了一次又一次地输入相同的对象结构 您在此处提到的大多数信息也适用于 JPA。我建议将 JPA 留在标签/问题中。 【参考方案1】:使用注释创建关系
假设所有类都用@Entity
和@Table
注释
单向一对一关系
public class Foo
private UUID fooId;
@OneToOne
private Bar bar;
public class Bar
private UUID barId;
//No corresponding mapping to Foo.class
由 Foo.class 管理的双向一对一关系
public class Foo
private UUID fooId;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "barId")
private Bar bar;
public class Bar
private UUID barId;
@OneToOne(mappedBy = "bar")
private Foo foo;
使用用户管理的联接表的单向一对多关系
public class Foo
private UUID fooId;
@OneToMany
@JoinTable(name="FOO_BAR",
joinColumns = @JoinColumn(name="fooId"),
inverseJoinColumns = @JoinColumn(name="barId"))
private List<Bar> bars;
public class Bar
private UUID barId;
//No Mapping specified here.
@Entity
@Table(name="FOO_BAR")
public class FooBar
private UUID fooBarId;
@ManyToOne
@JoinColumn(name = "fooId")
private Foo foo;
@ManyToOne
@JoinColumn(name = "barId")
private Bar bar;
//You can store other objects/fields on this table here.
在设置User
对象时非常常用,该对象具有可以执行的Role
列表。您可以向用户添加和删除角色,而不必担心级联删除Role
的问题。
使用外键映射的双向一对多关系
public class Foo
private UUID fooId;
@OneToMany(mappedBy = "bar")
private List<Bar> bars;
public class Bar
private UUID barId;
@ManyToOne
@JoinColumn(name = "fooId")
private Foo foo;
使用 Hibernate 托管连接表的双向多对多
public class Foo
private UUID fooId;
@OneToMany
@JoinTable(name="FOO_BAR",
joinColumns = @JoinColumn(name="fooId"),
inverseJoinColumns = @JoinColumn(name="barId"))
private List<Bar> bars;
public class Bar
private UUID barId;
@OneToMany
@JoinTable(name="FOO_BAR",
joinColumns = @JoinColumn(name="barId"),
inverseJoinColumns = @JoinColumn(name="fooId"))
private List<Foo> foos;
双向多对多使用用户管理的连接表对象
当您想要在连接对象上存储额外信息时通常使用,例如创建关系的日期。
public class Foo
private UUID fooId;
@OneToMany(mappedBy = "bar")
private List<FooBar> bars;
public class Bar
private UUID barId;
@OneToMany(mappedBy = "foo")
private List<FooBar> foos;
@Entity
@Table(name="FOO_BAR")
public class FooBar
private UUID fooBarId;
@ManyToOne
@JoinColumn(name = "fooId")
private Foo foo;
@ManyToOne
@JoinColumn(name = "barId")
private Bar bar;
//You can store other objects/fields on this table here.
确定双向关系的哪一方“拥有”关系:
这是建立 Hibernate 关系的一个比较棘手的方面,因为无论以何种方式建立关系,Hibernate 都会正确运行。唯一会改变的是外键存储在哪个表上。通常,您拥有集合的对象将拥有该关系。
示例:User
对象上声明了 Roles
列表。在大多数应用程序中,系统将比Roles
对象的实例更频繁地操作User
对象的实例。因此,我将使Role
对象成为关系的拥有方,并通过Role
的列表通过User
级联操作Role
对象。有关实际示例,请参阅双向一对多示例。通常,除非您有特殊要求,否则您将级联此方案中的所有更改。
确定您的 fetchType
延迟获取的集合在 SO 上导致的问题比我关心的要多,因为默认情况下 Hibernate 会延迟加载相关对象。根据 Hibernate 文档,关系是一对一还是多对多都没关系:
默认情况下,Hibernate 对集合使用惰性选择获取,对单值关联使用惰性代理获取。这些默认值适用于大多数应用程序中的大多数关联。
考虑一下我在何时在您的对象上使用 fetchType.LAZY
和 fetchType.EAGER
的两分钱。如果您知道 50% 的时间不需要访问父对象上的集合,我会使用 fetchType.LAZY
。
这样的性能优势是巨大的,并且只会随着您向集合中添加更多对象而增长。这是因为对于急切加载的集合,Hibernate 会在后台进行大量检查,以确保您的所有数据都没有过期。虽然我提倡将 Hibernate 用于集合,但请注意使用 fetchType.EAGER
会降低性能**。但是,以我们的 Person
对象为例。很有可能当我们加载Person
时,我们会想知道Roles
他们执行了什么。我通常会将这个集合标记为fetchType.EAGER
。 不要反射性地将您的收藏标记为fetchType.EAGER
只是为了绕过LazyInitializationException
。 这不仅对性能不利,而且通常表明您有设计问题。问问自己,这个集合是否真的是一个急切加载的集合,或者我这样做只是为了通过这种方法访问集合。 Hibernate 有办法解决这个问题,这不会对您的操作性能产生太大影响。如果你想为这个调用初始化一个延迟加载的集合,你可以在 Service
层中使用以下代码。
//Service Class
@Override
@Transactional
public Person getPersonWithRoles(UUID personId)
Person person = personDAO.find(personId);
Hibernate.initialize(person.getRoles());
return person;
对Hibernate.initialize
的调用会强制创建和加载集合对象。但是,请注意,如果您只将Person
实例传递给它,您将获得Person
的代理。请参阅documentation 了解更多信息。这种方法的唯一缺点是您无法控制 Hibernate 将如何实际获取您的对象集合。如果你想控制它,那么你可以在你的 DAO 中这样做。
//DAO
@Override
public Person findPersonWithRoles(UUID personId)
Criteria criteria = sessionFactory.getCurrentSession().createCritiera(Person.class);
criteria.add(Restrictions.idEq(personId);
criteria.setFetchMode("roles", FetchMode.SUBSELECT);
此处的性能取决于您指定的FetchMode
。我读过answers 说出于性能原因使用FetchMode.SUBSELECT
。如果您真的感兴趣,链接的答案会更详细。
如果您想在我重复自己的同时阅读我的内容,请随时查看我的其他答案here
确定级联方向
Hibernate 可以在双向关系中以一种或两种方式级联操作。因此,如果您在User
上有一个Role
的列表,您可以在两个方向上级联对Role
的更改。如果您更改了User
上特定Role
的名称,Hibernate 可以自动更新Role
表上关联的Role
。
然而,这并不总是需要的行为。如果您考虑一下,在这种情况下,根据对User
的更改对Role
进行更改没有任何意义。然而,朝着相反的方向前进是有道理的。在Role
对象本身上更改Role
的名称,并且该更改可以级联到具有Role
的所有User
对象。
就效率而言,通过保存User
对象所属的对象来创建/更新Role
对象是有意义的。这意味着您会将 @OneToMany
注释标记为级联注释。我举个例子:
public User saveOrUpdate(User user)
getCurrentSession.saveOrUpdate(user);
return user;
在上面的示例中,Hibernate 将为User
对象生成一个INSERT
查询,然后在将User
插入数据库后级联创建Role
。然后这些插入语句将能够使用 User
的 PK 作为它们的外键,因此您最终会得到 N + 1 个插入语句,其中 N 是用户列表中 Role
对象的数量。
相反,如果您想保存级联回 User
对象的单个 Role
对象,可以这样做:
//Assume that user has no roles in the list, but has been saved to the
//database at a cost of 1 insert.
public void saveOrUpdateRoles(User user, List<Roles> listOfRoles)
for(Role role : listOfRoles)
role.setUser(user);
getCurrentSession.saveOrUpdate(role);
这会导致 N + 1 次插入,其中 N 是 listOfRoles
中 Role
的数量,但还会生成 N 更新语句,因为 Hibernate 将每个 Role
添加到 User
表中.这种 DAO 方法比我们之前的方法具有更高的时间复杂度,O(n) 而不是 O(1),因为您必须遍历角色列表。尽可能避免这种情况。
然而,在实践中,通常关系的拥有方将是您标记级联的地方,并且您通常会级联所有内容。
孤儿删除
如果您删除与对象的所有关联,Hibernate 可以为您解决问题。假设您有一个User
,他有一个Role
的列表,并且在这个列表中是指向5 个不同角色的链接。假设您删除了一个名为 ROLE_EXAMPLE 的Role
,并且碰巧 ROLE_EXAMPLE 不存在于任何其他User
对象上。如果您在@OneToMany
注释上设置了orphanRemoval = true
,Hibernate 将通过级联从数据库中删除现在“孤立”的角色对象。
不应该在所有情况下都启用孤立删除。事实上,在我们上面的例子中使用 orphanRemoval 是没有意义的。仅仅因为没有User
可以执行 ROLE_EXAMPLE 对象所代表的任何操作,这并不意味着任何未来的User
将永远无法执行该操作。
本问答旨在补充 Hibernate 官方文档,其中包含大量用于这些关系的 XML 配置。
这些示例并不是要复制粘贴到生产代码中。它们是有关如何使用 JPA 注释在 Spring Framework 中配置 Hibernate 4 来创建和管理各种对象及其关系的通用示例。这些示例假定所有类都具有以下格式声明的 ID 字段:fooId
。此 ID 字段的类型不相关。
** 我们最近不得不放弃使用 Hibernate 来执行插入作业,在该作业中,我们通过集合将
免责声明: 我不知道这些示例是否适用于独立的 HIBERNATE
我与 Hibernate 或 Hibernate 开发团队没有任何关系。我提供了这些示例,以便在回答有关 Hibernate 标签的问题时可以参考。这些示例和讨论基于我自己的观点以及我如何使用 Hibernate 开发我的应用程序。这些例子绝不是全面的。我将它们基于我过去使用 Hibernate 的常见情况。
如果您在尝试实施这些示例时遇到问题,请不要发表评论并期待我解决您的问题。学习 Hibernate 的一部分是学习进出 的API。如果示例有错误,请随时编辑。
【讨论】:
我对使用 LAZY fetch 的建议与您不同。虽然我确实同意一般使用 LAZY fetch,但我不同意使用 hacky 代码触发延迟加载以避免延迟初始化异常的方法。相反,让 DAO 执行适当的连接获取等更合理,以便一起获取所需的属性。背后的原因正如您在引用的文章中提到的:更好地利用数据库资源。 你有一个你必须做的连接类型的例子吗?我没有包含它的唯一原因是我从来不需要使用 Criteria 手动加入,但如果你能给我一个例子,我会很乐意更新我的答案。 而且,我相信还值得一提的是双向一对多的关系拥有方的差异,在结果 SQL 方面。如果 ManyToOne 方拥有该关系,则会导致 1 + n 次插入和 n 次更新。在让 ManyToOne 拥有关系时,可以在插入时确定 FK,这将导致仅 1+n 次插入。 我通常使用 HQL,它的连接提取很容易。我确实希望使用 Criteria 不会更困难 :) 至于更好的未来参考,我可能会在引用的文章中简单地提出替代方案作为新答案。 请务必这样做。我刚刚快速浏览了一下,发现了一个名为Hibernate.initialize(Object object)
的方法,它强制初始化代理或集合。这听起来对我来说也是一个更好的解决方案。此外,如果您想建议对我关于生成的不同 SQL 的答案进行编辑,请随意。我坦率地承认我自己没有研究过 SQL 中的差异。以上是关于如何使用注解在 Hibernate 4 和 Spring 中定义不同类型的关系?的主要内容,如果未能解决你的问题,请参考以下文章