JPA EntityManager原生查询无法返回多对多关联的TableA数据
问题根源
这事儿的核心在于原生SQL查询和JPA的JPQL/HQL查询在关联实体加载逻辑上的本质差异:
Spring Data JPA的
findAll()为啥能正常获取关联数据?
Spring Data JPA的findAll()底层用的是JPQL查询(比如SELECT b FROM TableB b),JPA会严格遵循你在实体类上定义的@ManyToMany映射关系来处理关联数据。虽然你的TableB.aItems用的是默认的FetchType.LAZY(@ManyToMany默认懒加载),但因为你在@Transactional注解的方法里访问item.getAItems(),此时EntityManager还处于活跃状态,会自动触发懒加载——也就是执行额外的SQL去查询中间表a_b_mapping和table_a的数据,填充到aItems集合里。原生查询为啥拿不到关联数据?
当你用em.createNativeQuery("SELECT * FROM table_b", TableB.class)时,JPA做的仅仅是把table_b表的结果集字段直接映射到TableB实体的基本属性(比如id),完全不会处理你定义的关联映射。它不会主动去查询中间表和table_a的数据,哪怕你把aItems改成FetchType.EAGER也没用——因为原生SQL本身没有关联查询关联表,JPA没有足够的信息去加载关联数据,所以aItems始终是空集合(你的getter里初始化了空List,所以size是0)。
解决方案
根据你的需求,有几种靠谱的处理方式:
1. 改用JPQL查询(推荐)
直接用JPA的JPQL替代原生SQL,让JPA帮你处理关联加载:
// 替换原生查询为JPQL Query q = em.createQuery("SELECT b FROM TableB b", TableB.class); List<TableB> tableBItems = q.getResultList();
因为你在事务内访问item.getAItems(),会自动触发懒加载。如果想避免懒加载带来的N+1查询问题(每个TableB都查一次关联的TableA),可以用JOIN FETCH一次性加载所有关联数据:
Query q = em.createQuery("SELECT b FROM TableB b JOIN FETCH b.aItems", TableB.class); List<TableB> tableBItems = q.getResultList();
这种方式会生成一条包含table_b、a_b_mapping、table_a的JOIN查询,一次性把所有需要的数据查出来,效率更高。
2. (不推荐)手动处理原生查询的关联加载
如果你一定要用原生SQL,那得自己手动查询关联数据并填充:
Query q = em.createNativeQuery("SELECT * FROM table_b", TableB.class); List<TableB> tableBItems = q.getResultList(); for (TableB item : tableBItems) { // 手动查询当前TableB关联的所有TableA Query aQuery = em.createNativeQuery("SELECT a.* FROM table_a a JOIN a_b_mapping ab ON a.id = ab.a_id WHERE ab.b_id = ?", TableA.class); aQuery.setParameter(1, item.getId()); List<TableA> aItems = aQuery.getResultList(); item.setAItems(aItems); }
但这种方式会产生大量的额外查询,性能很差,除非有特殊需求,否则不建议用。
3. 调用EntityManager.refresh()触发加载
在拿到原生查询的TableB实例后,调用em.refresh(item),让JPA重新从数据库加载该实体及其关联数据(前提是关联的FetchType不是LAZY,或者在事务内):
for (TableB item : tableBItems) { em.refresh(item); // 现在aItems应该有数据了 item.getAItems().size(); }
不过这种方式同样会产生额外的查询,而且依赖于实体的加载策略,不是最优解。
内容的提问来源于stack exchange,提问作者Ph33ly

