使用@OrderColumn的JPA多关联查询返回大量null值求助
首先,咱们拆解下你遇到的两个核心问题:
1. @OrderColumn导致集合出现大量null的原因
你在Group的students集合上用了@OrderColumn(name = "id_student"),这是错误的用法!
@OrderColumn的设计是依赖一个专门存储集合元素顺序的列,而非实体的ID列。当你指定用id_student作为顺序列时,Hibernate会把这个列的值当成集合的索引位置:比如某个学生的id_student是8000,Hibernate就会创建一个长度为8001的List(索引从0到8000),把这个学生放在索引8000的位置,其他没有对应学生的索引位置就填充null——这就是你的集合里有7993个null、仅8个实际学生的原因。
2. MultiBagException的根源
Hibernate 5.x中,如果你同时对两个List类型的@OneToMany关联使用FETCH JOIN(且都是EAGER加载),就会触发这个异常。因为Hibernate无法处理多个"bag"类型集合(List属于bag的一种)同时通过join加载的场景。
具体解决方案
第一步:修复@OrderColumn的用法
你需要为集合顺序单独创建一列,而非复用实体ID:
- 在
student表中添加专门的顺序列,比如group_order INT NOT NULL DEFAULT 0,用来标记每个学生在所属组里的顺序。 - 修改
Group实体中的students字段注解:@OneToMany(mappedBy = "group", cascade = CascadeType.ALL, fetch = FetchType.EAGER) @OrderColumn(name = "group_order") // 使用专门的顺序列,而非学生ID private List<Student> students = new ArrayList<>(); - 如果之前给
Student的ratings加了@OrderColumn,同样要改成用专门的顺序列(比如在rating表加student_order列),不要用学生ID。
第二步:解决MultiBagException
下面提供3种可行方案,你可以根据业务需求选择:
方案A:将其中一个集合改为LAZY加载
把Student的ratings关联改为懒加载,避免一次查询加载两个EAGER集合:
@OneToMany(mappedBy = "student", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List<Rating> ratings = new ArrayList<>();
如果业务需要查询Group时同时加载ratings,可以用EntityGraph显式指定要抓取的关联,同时简化查询语句:
@EntityGraph(attributePaths = {"students.ratings"}) @Query("SELECT g FROM Group g WHERE g.id = :id AND (EXISTS (SELECT r FROM g.students s JOIN s.ratings r WHERE r.date BETWEEN :startMonth AND :endMonth) OR NOT EXISTS (SELECT r FROM g.students s JOIN s.ratings r))") Group findGroupByStudentGroupId(@Param("id") Long id, @Param("startMonth") Date startMonth, @Param("endMonth") Date endMonth);
方案B:将其中一个集合改为Set类型
Set不属于bag类型,Hibernate可以处理Set和List的同时FETCH JOIN。如果不需要维护学生的顺序,可以把Group的students改成Set:
@OneToMany(mappedBy = "group", cascade = CascadeType.ALL, fetch = FetchType.EAGER) private Set<Student> students = new HashSet<>();
移除@OrderColumn(Set是无序的),保留原来的查询语句,就不会触发MultiBagException了。
方案C:使用@Fetch(FetchMode.SUBSELECT)
在Group的students集合上添加@Fetch(FetchMode.SUBSELECT),让Hibernate用子查询加载集合,而非join:
@OneToMany(mappedBy = "group", cascade = CascadeType.ALL, fetch = FetchType.EAGER) @OrderColumn(name = "group_order") @Fetch(FetchMode.SUBSELECT) // 用子查询加载集合,避免join冲突 private List<Student> students = new ArrayList<>();
这种方式可以保留List和EAGER加载,同时避免MultiBagException,Hibernate会先查询Group,再用子查询加载所有关联学生,最后加载每个学生的评分。
额外优化:简化查询语句
你原来的查询里有冗余条件,比如s.group.id = g.id和r.student.id = s.id,这些都是JOIN已经隐含的条件,可以去掉,让查询更简洁。
内容的提问来源于stack exchange,提问作者Taras Danylchenko

