Hibernate原生查询主动加载懒加载实体的问题排查与解决
问题:原生查询下Lazy加载失效,额外触发关联实体查询的原因与解决办法
我正在优化架构创建接口的调用次数,解决N+1问题,已经通过命名查询把调用次数降到2次,现在想知道能不能用nativeQuery=true实现同样效果。
实体类定义
类A
@Entity @IdClass(AId.class) @Table(name ="A") @Data class A implements Serializable{ @Id @Column(name = "ID") private Integer id; // 省略其他代码... @ManyToOne(fetch=FetchType.Lazy) @JoinColumns( ... ) private B b; }
类B
@Entity @IdClass(AId.class) @Table(name ="B") @Data class B implements Serializable{ @Id @Column(name = "ID") private Integer id; // 省略其他代码... @OneToMany(mappedBy="b", cascade=Cascade.ALL, fetch=FetchType.Lazy) @JoinColumns( ... ) private List<A> aList; }
遇到的问题
尽管A中B的加载方式是FetchType.Lazy,但不管用生成查询还是原生查询获取A时,都会额外加载B实体。
预期只执行这条查询:
Hibernate: /* dynamic native SQL query */ SELECT * FROM A a WHERE a.ID IN ( ? )
但实际总会多执行一条B的查询:
Hibernate: /* load com.example.to.B */ select b0_.id as id1_1_0_, from B b0_ where b0_.id=?
想知道该现象的原因是什么,是否有办法避免?
原因分析
- 原生查询的实体映射特性:当用原生查询返回JPA实体(比如
createNativeQuery(sql, A.class))时,Hibernate需要将结果集映射为实体对象。即使标注了FetchType.Lazy,Hibernate为了保证关联字段的完整性,会主动验证关联的B是否存在,或者因映射逻辑触发关联实体加载。 - Lombok @Data的副作用:
@Data会自动生成equals()、hashCode()和toString()方法。如果这些方法中用到了关联属性B(比如toString()打印B的信息),代码中不经意调用这些方法(如日志打印、调试、集合操作)时,就会触发Lazy加载初始化,导致额外查询。 - 会话上下文的自动初始化:在Hibernate会话未关闭时,若访问A对象的b属性(哪怕是间接访问),Lazy代理就会触发数据库查询。
解决办法
方法1:使用DTO投影替代实体返回
不要直接返回A实体,定义只包含A所需字段的DTO类,用原生查询直接映射到DTO,完全避免触发关联实体加载。
示例DTO:
@Data class ADTO { private Integer id; // 只保留A需要的字段,不包含B相关属性 }
查询代码:
String sql = "SELECT id, ... FROM A a WHERE a.ID IN (?)"; Query query = entityManager.createNativeQuery(sql, ADTO.class); List<ADTO> result = query.getResultList();
方法2:自定义结果集映射,跳过关联属性
若必须返回A实体,可通过@SqlResultSetMapping自定义映射规则,仅映射A自身字段,不处理关联的B属性。
定义映射:
@SqlResultSetMapping( name = "AMapping", classes = @ConstructorResult( targetClass = A.class, columns = { @ColumnResult(name = "ID"), // 映射A的其他字段,不包含B的关联字段 } ) )
查询代码:
String sql = "SELECT id, ... FROM A a WHERE a.ID IN (?)"; Query query = entityManager.createNativeQuery(sql, "AMapping"); List<A> result = query.getResultList();
方法3:修复Lombok自动生成方法的影响
手动重写equals()、hashCode()和toString()方法,排除关联属性B,避免因这些方法触发Lazy加载。
示例:
@Entity @IdClass(AId.class) @Table(name ="A") @Getter @Setter @ToString(exclude = "b") // 排除b字段 class A implements Serializable{ @Id @Column(name = "ID") private Integer id; @ManyToOne(fetch=FetchType.Lazy) @JoinColumns(...) private B b; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; A a = (A) o; return Objects.equals(id, a.id); // 仅比较自身主键 } @Override public int hashCode() { return Objects.hash(id); } }
方法4:控制会话生命周期
确保在查询完A后立即关闭会话(EntityManager),或者将实体转换为DTO再返回。这样即使后续误访问b属性,也会因会话关闭无法触发额外查询(会抛出LazyInitializationException,但不会产生数据库请求)。
内容的提问来源于stack exchange,提问作者Amadis
相关产品推荐
相关产品推荐

