EJBTransactionRolledBackException排查:大量数据下TypedQuery分页异常
嘿,我来帮你拆解这个问题~首先你的怀疑方向是对的,但具体原因要结合LEFT JOIN FETCH和JPA的分页机制来看:
为什么会出现这个问题?
当你在查询中使用LEFT JOIN FETCH加载关联实体时,尤其是关联的是集合(OneToMany)时,数据库会返回笛卡尔积结果集——简单说就是每条Person会和它关联的City、User组合成一行记录,如果一个Person对应多个City,结果集行数会远大于Person的实际数量。
而JPA(比如Hibernate)在处理带DISTINCT和JOIN FETCH的分页查询时,无法直接在数据库层面完成分页:它会先把所有笛卡尔积结果加载到内存中,执行DISTINCT去重得到唯一的Person列表,再根据setFirstResult和setMaxResults截取分页数据。这就导致即使你只需要前5条数据,JPA也会先把15万条Person对应的所有关联数据全部加载到内存,直接触发内存溢出(OOM),最终导致事务回滚抛出EJBTransactionRolledBackException。
哪怕你的关联是ManyToOne(每个Person对应一个City/User),15万条实体对象一次性加载到内存也会占用大量堆空间,同样可能触发OOM。
优化方案
方案1:分两次查询(最稳妥的通用方案)
先分页获取Person的ID列表,再根据ID批量查询并加载关联实体。这样既避免了笛卡尔积,又能让分页在数据库层面执行,内存占用大幅降低:
@Override public List<Person> findPaginated(Integer startPosition, Integer maxResult) { // 第一步:分页查询Person的ID,这一步在数据库层面完成分页 TypedQuery<Long> idQuery = em.createQuery("SELECT p.id FROM Person p ORDER BY p.id", Long.class); if (startPosition != null) { idQuery.setFirstResult(startPosition); } if (maxResult != null) { idQuery.setMaxResults(maxResult); } List<Long> personIds = idQuery.getResultList(); if (personIds.isEmpty()) { return Collections.emptyList(); } // 第二步:根据ID列表查询Person并加载关联实体 TypedQuery<Person> personQuery = em.createQuery( "SELECT DISTINCT p FROM Person p LEFT JOIN FETCH p.cityByCity LEFT JOIN FETCH p.userByUser " + "WHERE p.id IN :ids ORDER BY p.id", Person.class ); personQuery.setParameter("ids", personIds); return personQuery.getResultList(); }
方案2:使用子查询Fetch模式(Hibernate专属)
如果你的JPA实现是Hibernate 5.2+,可以在Person实体的关联字段上添加@Fetch(FetchMode.SUBSELECT)注解,让Hibernate先分页查询Person,再通过子查询批量加载关联实体,避免笛卡尔积:
// 在Person实体的关联字段上添加注解 @ManyToOne(fetch = FetchType.LAZY) @Fetch(FetchMode.SUBSELECT) private City cityByCity; @ManyToOne(fetch = FetchType.LAZY) @Fetch(FetchMode.SUBSELECT) private User userByUser;
然后简化查询语句,去掉JOIN FETCH,Hibernate会自动处理关联加载:
@Override public List<Person> findPaginated(Integer startPosition, Integer maxResult) { TypedQuery<Person> findAllQuery = em.createQuery("SELECT p FROM Person p ORDER BY p.id", Person.class); if (startPosition != null) { findAllQuery.setFirstResult(startPosition); } if (maxResult != null) { findAllQuery.setMaxResults(maxResult); } return findAllQuery.getResultList(); }
这种方式下,Hibernate会先执行分页查询Person,再通过子查询(比如SELECT ... FROM City WHERE id IN (SELECT cityByCity_id FROM Person WHERE ...))批量加载所有关联的City和User,内存压力小很多。
方案3:调整内存配置(临时应急)
如果只是临时需要处理大量数据,可以尝试调大JVM堆内存(比如设置-Xmx4g),但这只是治标不治本的方法,随着数据量增长还是会出现问题,建议优先采用前两种方案。
内容的提问来源于stack exchange,提问作者Jeredriq Demas

