升级Hibernate 6后QueryDSL集合查询失效,求修复方案
问题背景
升级Hibernate至最新版本后,原有的三个查询指定单一角色用户的QueryDSL/JPA查询方法均抛出异常,以下是各方法的异常信息及适配修复方案。
各方法异常详情
- findByUserRole_1() 抛出异常:
org.hibernate.query.SemanticException: Operand of 'member of' operator must be a plural path
- findByUserRole_2()(替代where子句实现)抛出异常:
org.hibernate.query.QueryArgumentException: Argument [ROLE_ADMIN] of type [com.thevegcat.app.config.UserRole] did not match parameter type [java.util.Set (n/a)]
- findByUserRole_3()(原正常运行方法)抛出异常:
java.lang.ClassCastException: class org.hibernate.metamodel.mapping.internal.BasicAttributeMapping cannot be cast to class org.hibernate.sql.ast.tree.from.TableGroupJoinProducer (org.hibernate.metamodel.mapping.internal.BasicAttributeMapping and org.hibernate.sql.ast.tree.from.TableGroupJoinProducer are in unnamed module of loader 'app')
相关代码定义
UserRole.java
public enum UserRole { ROLE_ADMIN, ROLE_ADVANCED, ROLE_EDITOR, ROLE_USER, ROLE_ANONYMOUS }
User.java
@Entity public class User { // ... @Convert(converter = UserRolesConverter.class) @Column(columnDefinition = "TEXT") private Set<UserRole> roles; // ... }
UserRepositoryImpl.java
public class UserRepositoryImpl // ... public List<User> findByUserRole_1( final UserRole userRole ) { final QUser user = QUser.user; return new JPAQueryFactory(this.entityManager) .selectFrom(user) .where( new BooleanBuilder().and(user.roles.contains(userRole)).getValue() ) .fetch(); } public List<User> findByUserRole_2( final UserRole userRole ) { final QUser user = QUser.user; return new JPAQueryFactory(this.entityManager) .selectFrom(user) .where( new BooleanBuilder().and(user.roles.any().eq(userRole)).getValue() ) .fetch(); } // ... }
UserRepository.java
public interface UserRepository extends PagingAndSortingRepository<User, Integer>, CrudRepository<User, Integer> { // ... @Query("SELECT u FROM User u LEFT JOIN FETCH u.roles r WHERE :role IN ( r )") List<User> findByUserRole_3(UserRole role); // ... }
适配Hibernate 6的修复方案
核心原因:Hibernate 6对@Convert标注的转换集合属性(如Set<UserRole>)的处理逻辑变更,不再将其视为JPA标准集合关联,而是作为基础属性处理。
修复findByUserRole_1()
Hibernate 6不支持对转换后的集合使用contains()生成MEMBER OF查询,需改用字符串匹配或数据库JSON函数查询(依赖UserRolesConverter的序列化格式):
public List<User> findByUserRole_1(final UserRole userRole) { final QUser user = QUser.user; String roleStr = userRole.name(); // 若转换器将集合序列化为逗号分隔字符串 return new JPAQueryFactory(this.entityManager) .selectFrom(user) .where(user.roles.stringValue().contains("," + roleStr + ",")) .fetch(); }
如果转换器序列化为JSON数组,可使用数据库JSON函数(以PostgreSQL为例):
.where(Expressions.booleanTemplate("jsonb_contains({0}, '{\"\"}{1}{\"\"}')", user.roles, roleStr))
修复findByUserRole_2()
any()方法仅适用于JPA关联集合,不适用于转换后的基础属性,直接替换为字符串匹配或JSON函数查询,逻辑与findByUserRole_1()一致。
修复findByUserRole_3()
不能对转换后的集合属性使用JOIN FETCH,需修改JPQL查询,改用字符串或JSON函数匹配:
@Query("SELECT u FROM User u WHERE FUNCTION('JSON_CONTAINS', u.roles, :roleJson) = 1") List<User> findByUserRole_3(@Param("roleJson") String roleJson);
调用时传入序列化后的角色JSON字符串:
userRepository.findByUserRole_3("[\"" + UserRole.ROLE_ADMIN.name() + "\"]");
推荐替代方案:改用@ElementCollection
如果业务允许,将@Convert替换为@ElementCollection,让Hibernate将角色映射为关联表,原有查询逻辑可直接兼容Hibernate 6:
@Entity public class User { // ... @ElementCollection(targetClass = UserRole.class) @Enumerated(EnumType.STRING) @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id")) @Column(name = "role") private Set<UserRole> roles; // ... }
此方案符合JPA标准规范,无需修改原有查询代码,避免依赖转换器的序列化格式。
内容的提问来源于stack exchange,提问作者user1034461

