You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JPA/Hibernate使用select new构造表达式触发逐行懒加载问题

Hibernate构造器表达式(select new)触发N+1查询问题分析

近期在JPA/Hibernate中使用select new/constructor expression(构造器表达式)时,观察到不符合预期的行为:该语法会对结果集中每一行的实体触发类懒加载的查询逻辑,执行效率极低。

测试示例

实体类定义代码:

@Value
public class PojoTuple {
    Entity1 e1;
    Entity2 e2;
}

@Entity
@Table(name = "entity1", schema = DEFAULT_DATABASE_SCHEMA)
@NoArgsConstructor(access = PROTECTED)
public class Entity1 {

    @Id
    @Column(name = "id", nullable = false)
    private String id;

    @Column(name = "field1", nullable = false)
    private String field1;
}

@Entity
@Table(name = "entity2", schema = DEFAULT_DATABASE_SCHEMA)
@NoArgsConstructor(access = PROTECTED)
public class Entity2 {

    @Id
    @Column(name = "id", nullable = false)
    private String id;

    @Column(name = "fkentity1", nullable = false)
    private String entity1Id;

    @Column(name = "field2", nullable = false)
    private String field2;
}

建表语句与测试数据:

create table entity1
(
    id     varchar2(22 char) not null primary key,
    field1 varchar2(50 char) not null
);

create table entity2
(
    id        varchar2(22 char) not null primary key,
    fkentity1 varchar2(22 char) not null,
    field2    varchar2(50 char) not null

);

insert into entity1 (id, field1) values ('10', 'anyvalue1');
insert into entity1 (id, field1) values ('11', 'anyvalue2');

insert into entity2 (id, fkentity1, field2) VALUES ('20', '10', 'anyvalue3');
insert into entity2 (id, fkentity1, field2) VALUES ('21', '11', 'anyvalue4');

场景一:使用select new语法查询

采用select new构造PojoTuple的方式编写JPQL:

Query query = entityManager.createQuery("select new my.package.PojoTuple(e1, e2) " +
                                        "from Entity1 e1 " +
                                        "join Entity2 e2 on e1.id=e2.entity1Id ");
query.getResultList();

执行时首先仅发起1条查询获取e1、e2的主键ID,随后针对结果集的每一行,逐个按ID发起查询获取e1、e2的完整属性,产生大量额外查询,执行SQL日志如下:

Query:["select entity1x0_.id as col_0_0_, entity2x1_.id as col_1_0_
from schema.entity1 entity1x0_ inner join schema.entity2 entity2x1_ on
(entity1x0_.id=entity2x1_.fkentity1)"]

Query:["select entity1x0_.id as id1_1_0_, entity1x0_.field1 as
field2_1_0_ from schema.entity1 entity1x0_ where entity1x0_.id=?"]
Params:[(10)]

Query:["select entity2x0_.id as id1_2_0_, entity2x0_.fkentity1 as
fkentity2_2_0_, entity2x0_.field2 as field3_2_0_ from schema.entity2
entity2x0_ where entity2x0_.id=?"] Params:[(20)]

Query:["select entity1x0_.id as id1_1_0_, entity1x0_.field1 as
field2_1_0_ from schema.entity1 entity1x0_ where entity1x0_.id=?"]
Params:[(11)]

Query:["select entity2x0_.id as id1_2_0_, entity2x0_.fkentity1 as
fkentity2_2_0_, entity2x0_.field2 as field3_2_0_ from schema.entity2
entity2x0_ where entity2x0_.id=?"] Params:[(21)]

场景二:直接查询实体返回

不使用select new,直接查询关联实体:

Query query = entityManager.createQuery("select e1, e2 " +
                                        "from Entity1 e1 " +
                                        "join Entity2 e2 on e1.id=e2.entity1Id ");

query.getResultList();

该写法仅发起1条SQL即可查询所有需要的字段,无额外查询,执行SQL日志如下:

Query:["select entity1x0_.id as id1_1_0_, entity2x1_.id as id1_2_1_,
entity1x0_.field1 as field2_1_0_, entity2x1_.fkentity1 as
fkentity2_2_1_, entity2x1_.field2 as field3_2_1_ from schema.entity1
entity1x0_ inner join schema.entity2 entity2x1_ on
(entity1x0_.id=entity2x1_.fkentity1)"]
Params:[()]

问题解答

这个现象不属于特殊设计机制,也不算严格意义上的功能bug,本质是Hibernate 5.x系列版本在构造器表达式场景下的优化缺失,核心逻辑如下:

  1. 直接写select e1,e2返回实体时,Hibernate查询计划器明确识别到返回结果需要放入持久化上下文(一级缓存),因此会直接在主SQL中查询实体的所有基础字段,一次性完成实体实例化,不会产生额外查询。
  2. 使用select new PojoTuple(e1,e2)时,5.x版本的Hibernate不会将构造器参数中的实体识别为「需要完全初始化的托管实体」,只会先查询出实体的主键标识,在构造PojoTuple传入实体参数的环节,触发实体代理的懒加载逻辑,也就是按主键二次查询完整实体,最终演变为N+1问题。
  3. 该问题在Hibernate 6.0+版本已经完成修复,新版本会对构造器中传入的实体执行和直接select实体一致的字段预取逻辑,不会再产生额外查询。

5.x版本临时解决方案

暂时无法升级Hibernate版本时,可通过以下方式规避问题:

  • 不将完整实体传入构造器,直接把需要使用的字段拆出来传入,例如select new PojoTuple(e1.id, e1.field1, e2.id, e2.field2),同步调整PojoTuple的字段为对应基础类型,完全避免实体加载逻辑。
  • 放弃select new写法,直接查询返回List<Object[]>结果,遍历结果手动取出e1、e2组装PojoTuple。这种方式下主查询已经拉取了实体全字段,e1、e2在持久化上下文中是完全初始化状态,不会触发额外查询。
  • 如果实体间定义了正式关联关系,可在JPQL中添加join fetch强制Hibernate在主SQL中抓取实体全字段。

内容的提问来源于stack exchange,提问作者mkobylarz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.01 04:42:27