如何使用注解在 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 【问题描述】:

我有两个类,FooBar,如下:

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.LAZYfetchType.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 是 listOfRolesRole 的数量,但还会生成 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 中定义不同类型的关系?的主要内容,如果未能解决你的问题,请参考以下文章

Hibernate注解

Hibernate中@Embedded和@Embeddable注解

如何将hibernate注解应用于抽象类的子类

Hibernate注解配置与XML配置区别

hibernate注解与jpa注解

Hibernate注解配置