JPA/Hibernate使用select new构造表达式触发逐行懒加载问题
近期在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系列版本在构造器表达式场景下的优化缺失,核心逻辑如下:
- 直接写
select e1,e2返回实体时,Hibernate查询计划器明确识别到返回结果需要放入持久化上下文(一级缓存),因此会直接在主SQL中查询实体的所有基础字段,一次性完成实体实例化,不会产生额外查询。 - 使用
select new PojoTuple(e1,e2)时,5.x版本的Hibernate不会将构造器参数中的实体识别为「需要完全初始化的托管实体」,只会先查询出实体的主键标识,在构造PojoTuple传入实体参数的环节,触发实体代理的懒加载逻辑,也就是按主键二次查询完整实体,最终演变为N+1问题。 - 该问题在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

