JPA 急切未加入

Posted

技术标签:

【中文标题】JPA 急切未加入【英文标题】:JPA eager fetch does not join 【发布时间】:2010-10-02 13:51:28 【问题描述】:

JPA 的 fetch 策略究竟控制什么?我无法发现急切和懒惰之间的任何区别。在这两种情况下,JPA/Hibernate 都不会自动加入多对一关系。

示例:人只有一个地址。一个地址可以属于很多人。 JPA 注释的实体类如下所示:

@Entity
public class Person 
    @Id
    public Integer id;

    public String name;

    @ManyToOne(fetch=FetchType.LAZY or EAGER)
    public Address address;


@Entity
public class Address 
    @Id
    public Integer id;

    public String name;

如果我使用 JPA 查询:

select p from Person p where ...

JPA/Hibernate 生成一个 SQL 查询以从 Person 表中进行选择,然后为 每个 人生成一个不同的地址查询:

select ... from Person where ...
select ... from Address where id=1
select ... from Address where id=2
select ... from Address where id=3

这对于大型结果集非常不利。如果有 1000 人,它会生成 1001 个查询(1 个来自 Person,1000 个来自 Address)。我知道这一点是因为我正在查看 mysql 的查询日志。我的理解是,将地址的获取类型设置为 eager 会导致 JPA/Hibernate 自动使用连接进行查询。但是,无论获取类型如何,它仍然会为关系生成不同的查询。

只有当我明确告诉它加入时,它才会真正加入:

select p, a from Person p left join p.address a where ...

我在这里遗漏了什么吗?我现在必须手动编写每个查询的代码,以便它离开多对一关系。我在 MySQL 中使用 Hibernate 的 JPA 实现。

编辑:看来(参见 Hibernate FAQ here 和 here)FetchType 不会影响 JPA 查询。所以就我而言,我已经明确告诉它加入。

【问题讨论】:

FAQ 条目的链接已损坏,这里正在运行one 【参考方案1】:

JPA 没有提供任何关于映射注释以选择获取策略的规范。通常,可以通过以下任何一种方式获取相关实体

SELECT => 一次查询根实体 + 一次查询相关映射实体/每个根实体的集合 = (n+1) 次查询 SUBSELECT => 一个查询根实体 + 第二个查询相关映射实体/在第一个查询中检索到的所有根实体的集合 = 2 个查询 JOIN => 一个查询来获取根实体及其所有映射实体/集合 = 1 个查询

所以SELECTJOIN 是两个极端,SUBSELECT 介于两者之间。可以根据自己的领域模型选择合适的策略。

默认情况下,JPA/EclipseLink 和 Hibernate 都使用 SELECT。这可以通过使用覆盖:

@Fetch(FetchMode.JOIN) 
@Fetch(FetchMode.SUBSELECT)

处于休眠状态。它还允许使用@Fetch(FetchMode.SELECT) 显式设置SELECT 模式,可以通过使用批量大小进行调整,例如@BatchSize(size=10).

EclipseLink中对应的注解有:

@JoinFetch
@BatchFetch

【讨论】:

为什么会有这些设置?我认为几乎总是必须使用 JOIN。现在我必须用特定于休眠的注释标记所有映射。 有趣但遗憾的是@Fetch(FetchMode.JOIN) 对我来说根本不起作用(Hibernate 4.2.15),在 JPQL 和 Criteria 中一样。 Hibernate 注释似乎对我也不起作用,使用 Spring JPA @Aphax 这可能是因为 Hibernate 对 JPAQL/Criteria 与 em.find() 使用了不同的默认策略。请参阅vladmihalcea.com/2013/10/17/… 和参考文档。 @vbezhenar(以及稍后阅读他的评论的其他人):JOIN 查询在数据库中生成笛卡尔积,因此您应该确定要计算该笛卡尔积。请注意,如果您使用 fetch join,即使您放 LAZY,它也会被预先加载。【参考方案2】:

“mxc”是对的。 fetchType 只是指定何时应该解决关系。

要通过使用外部连接来优化急切加载,您必须添加

@Fetch(FetchMode.JOIN)

到你的领域。这是一个特定于休眠的注解。

【讨论】:

这对我来说不适用于 Hibernate 4.2.15,在 JPQL 或 Criteria 中。 @Aphax 我认为这是因为 JPAQL 和 Criteria 不遵守 Fetch 规范。 Fetch 注释仅适用于 em.find()、AFAIK。请参阅vladmihalcea.com/2013/10/17/… 另外,请参阅休眠文档。我很确定这在某处被覆盖。 @JoshuaDavis 我的意思是 \@Fetch 注释在查询中没有应用任何类型的 JOIN 优化,无论是 JPQL 还是 em.find(),我只是在 Hibernate 5.2 上再次尝试.+ 还是一样的【参考方案3】:

fetchType 属性控制在获取主实体时是否立即获取带注释的字段。它不一定规定如何构造 fetch 语句,实际的 sql 实现取决于您使用的提供程序 toplink/hibernate 等。

如果您设置fetchType=EAGER 这意味着注释字段与实体中的其他字段同时填充其值。因此,如果您打开实体管理器检索您的人员对象,然后关闭实体管理器,随后执行 person.address 将不会导致引发延迟加载异常。

如果您设置fetchType=LAZY,则该字段仅在访问时才会填充。如果您已经关闭了实体管理器,那么如果您执行 person.address 将引发延迟加载异常。要加载字段,您需要使用 em.merge() 将实体放回 entitymangers 上下文中,然后进行字段访问,然后关闭 entitymanager。

在构建包含客户订单集合的客户类时,您可能需要延迟加载。如果您在想要获取客户列表时检索了客户的每个订单,那么当您只查找客户姓名和联系方式时,这可能是一项昂贵的数据库操作。最好将数据库访问权限留到以后。

对于问题的第二部分——如何让hibernate生成优化的SQL?

Hibernate 应该允许您提供有关如何构造最有效查询的提示,但我怀疑您的表构造有问题。表中是否建立了关系? Hibernate 可能已经决定一个简单的查询将比一个连接更快,尤其是在缺少索引等的情况下。

【讨论】:

【参考方案4】:

尝试:

select p from Person p left join FETCH p.address a where...

它对我来说与 JPA2/EclipseLink 类似,但似乎此功能存在于 JPA1 too 中:

【讨论】:

【参考方案5】:

如果您使用 EclipseLink 而不是 Hibernate,您可以通过“查询提示”优化您的查询。请参阅 Eclipse Wiki 中的这篇文章:EclipseLink/Examples/JPA/QueryOptimization。

有一个章节是关于“加入阅读”的。

【讨论】:

【参考方案6】:

加入你可以做很多事情(使用eclipselink)

在 jpql 中你可以做 left join fetch

在命名查询中可以指定查询提示

在 TypedQuery 中你可以这样说

query.setHint("eclipselink.join-fetch", "e.projects.milestones");

还有批量获取提示

query.setHint("eclipselink.batch", "e.address");

http://java-persistence-performance.blogspot.com/2010/08/batch-fetching-optimizing-object-graph.html

【讨论】:

【参考方案7】:

我确实遇到了这个问题,只是 Person 类有一个嵌入的键类。 我自己的解决方案是将它们加入查询中并 remove

@Fetch(FetchMode.JOIN)

我的嵌入式 id 类:

@Embeddable
public class MessageRecipientId implements Serializable 

    @ManyToOne(targetEntity = Message.class, fetch = FetchType.LAZY)
    @JoinColumn(name="messageId")
    private Message message;
    private String governmentId;

    public MessageRecipientId() 
    

    public Message getMessage() 
        return message;
    

    public void setMessage(Message message) 
        this.message = message;
    

    public String getGovernmentId() 
        return governmentId;
    

    public void setGovernmentId(String governmentId) 
        this.governmentId = governmentId;
    

    public MessageRecipientId(Message message, GovernmentId governmentId) 
        this.message = message;
        this.governmentId = governmentId.getValue();
    


【讨论】:

【参考方案8】:

我想到了两件事。

首先,您确定您的地址是指 ManyToOne 吗?这意味着多个人将拥有相同的地址。如果它是为其中一个编辑的,它将为所有这些编辑。这是你的意图吗? 99% 的时间地址都是“私人的”(从某种意义上说,它们只属于一个人)。

其次,您是否对 Person 实体有任何其他急切的关系?如果我没记错的话,Hibernate 只能处理一个实体上的一个急切关系,但这可能是过时的信息。

我这么说是因为从我现在的位置来看,您对它应该如何工作的理解基本上是正确的。

【讨论】:

这是一个使用多对一的虚构示例。个人地址可能不是最好的例子。我在我的代码中没有看到任何其他急切的 fetch 类型。 我的建议是将其简化为一个简单的示例,该示例可以运行并执行您所看到的操作,然后发布。您的模型中可能存在导致意外行为的其他复杂情况。 我完全按照上面显示的方式运行了代码,它表现出上述行为。 我从陌生的地方(有时称为城市)听说,多人共享同一个地址(这里有大到足以容纳一个人以上的建筑物),甚至一间公寓里有多个人。 - 我们生活在@cletus 的陌生世界

以上是关于JPA 急切未加入的主要内容,如果未能解决你的问题,请参考以下文章

JPA Annotation ElementCollection 在 Spring Boot 中急切获取

@ElementCollection 的 JPA 延迟加载

带有两个子查询/连接的 JPA 标准选择

Spring boot + jpa 懒取

直接加入 JPA 或 HIBERNATE

不加入子表的 JPA (@OneToMany) 查询