如何通过Criteria Select正确构造含@OneToMany关联的BooksOfAuthor
解决Criteria API构造DTO时的集合类型匹配问题
这个问题我之前也碰到过,本质是Hibernate处理关联集合的类型映射,加上DTO构造函数参数不匹配,再叠上懒加载的坑在作祟。我们一步步拆解解决:
错误原因分析
报错提示Unable to locate appropriate constructor on class [BooksOfAuthor]. Expected arguments are: String, java.util.Collection,核心问题有两个:
- 集合类型不匹配:你定义的
BooksOfAuthor构造函数参数是List<Book>,但Hibernate在处理@OneToMany关联集合时,默认返回的是它自己实现的Collection子类(比如PersistentBag)。虽然List是Collection的子类,但Hibernate的构造函数匹配逻辑会严格检查参数类型,导致无法识别兼容的子类类型。 - 懒加载与关联抓取问题:即使类型匹配,你当前的代码没有显式抓取
books集合,会触发懒加载,在构造DTO时可能抛出LazyInitializationException,同时还会导致N+1查询的性能问题。
解决方案
方案一:调整DTO构造函数参数类型(推荐)
把BooksOfAuthor的构造函数参数改为Collection<Book>,然后在内部转成List,这样就能匹配Hibernate返回的集合类型:
public class BooksOfAuthor { private String authorName; private List<Book> books; // 修改构造函数参数为Collection<Book> public BooksOfAuthor(String authorName, Collection<Book> books) { this.authorName = authorName; // 转成ArrayList确保集合是可控的非Hibernate代理类型 this.books = new ArrayList<>(books); } // Getters and Setters }
然后修改Criteria查询,添加fetch join来预加载集合,同时添加distinct避免笛卡尔积导致的重复结果:
public List<BooksOfAuthor> simplifiedSelect() { CriteriaBuilder builder = manager.getCriteriaBuilder(); CriteriaQuery<BooksOfAuthor> criteria = builder.createQuery(BooksOfAuthor.class); Root<Author> root = criteria.from(Author.class); // 显式LEFT JOIN FETCH books集合,预加载避免懒加载问题 root.fetch(Author_.books, JoinType.LEFT); // 构造DTO,参数现在匹配Collection类型 criteria.select(builder.construct(BooksOfAuthor.class, root.get(Author_.name), root.get(Author_.books))); // 去重,因为fetch join会产生笛卡尔积,导致同一Author对应多条结果 criteria.distinct(true); TypedQuery<BooksOfAuthor> query = manager.createQuery(criteria); return query.getResultList(); }
方案二:保持List构造函数,使用JPQL强制类型转换
如果不想修改DTO的构造函数,可以用JPQL查询,通过CAST函数把集合强制转成List<Book>:
public List<BooksOfAuthor> simplifiedSelect() { // 使用CAST将集合转成java.util.List,匹配DTO构造函数 String jpql = "SELECT new BooksOfAuthor(a.name, CAST(a.books AS java.util.List)) " + "FROM Author a LEFT JOIN FETCH a.books"; TypedQuery<BooksOfAuthor> query = manager.createQuery(jpql, BooksOfAuthor.class); // 添加distinct去重,避免笛卡尔积重复 query.setHint("org.hibernate.query.distinct", true); return query.getResultList(); }
注意事项
- 使用
LEFT JOIN FETCH是为了确保即使没有书籍的作者也能被查询到,如果只需要有书籍的作者,可以换成INNER JOIN FETCH。 distinct是必须的,因为fetch join会产生笛卡尔积(每个书籍对应一条作者记录),去重后才能得到每个作者唯一的DTO实例。
内容的提问来源于stack exchange,提问作者Purport Tees
相关产品推荐
相关产品推荐

