Spring Boot 3+Hibernate 6.4+原生查询无法用实体作参数,求兼容配置
Spring Boot 3.x升级后原生查询实体参数失效的解决方案
问题背景
Spring Boot 2.x升级到3.x后,原生查询中直接传递实体对象作为参数的写法全部失效,必须改为传递实体ID才能正常执行。相关代码与报错信息如下:
旧代码(Spring Boot 2.x有效)
@Query(value = "SELECT DISTINCT o.a from table as o WHERE o.user= :user ORDER BY o.a", nativeQuery = true) public List<String> nativeFindDistinctShippingMethodByUserOrderByShippingMethod(@Param("user") User user);
升级后报错信息
[ERROR] 2024-08-26 07:26:46.775 [http-nio-127.0.0.1-8002-exec-4] [dispatcherServlet].log() - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.InvalidDataAccessApiUsageException: Could not resolve NativeQuery parameter type : `org.hibernate.query.internal.QueryParameterNamedImpl@2c0b7d22`] with root cause java.lang.IllegalArgumentException: Could not resolve NativeQuery parameter type : `org.hibernate.query.internal.QueryParameterNamedImpl@2c0b7d22` at org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl.<init>(JdbcParameterBindingsImpl.java:78) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] at org.hibernate.query.sql.internal.NativeSelectQueryPlanImpl.performList(NativeSelectQueryPlanImpl.java:126) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final] at org.hibernate.query.sql.internal.NativeQueryImpl.doList(NativeQueryImpl.java:628) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
临时可行代码(传递ID)
@Query(value = "SELECT DISTINCT o.a from table as o WHERE o.user= :user ORDER BY o.a", nativeQuery = true) public List<String> nativeFindDistinctShippingMethodByUserOrderByShippingMethod(@Param("user") Long userId);
原因分析
Spring Boot 3.x默认搭配Hibernate 6.x,后者对原生查询的参数绑定逻辑做了破坏性变更:
- 不再支持隐式将实体对象转换为其主键值绑定到SQL参数
- 该变更旨在避免歧义(如复合主键实体、实体多字段可映射等场景),强制参数绑定的显式性
是否有配置开关恢复旧行为?
目前Hibernate 6.x和Spring Boot 3.x无官方配置开关直接恢复原生查询传递实体参数的旧行为,但可通过以下自定义方案实现类似效果:
方案1:自定义Hibernate参数解析器
实现Hibernate的ParameterBinder,自动将实体类型参数转换为其主键值:
import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.query.spi.QueryParameterImplementor; import java.io.Serializable; import java.sql.PreparedStatement; import java.sql.SQLException; public class EntityIdParameterBinder implements org.hibernate.sql.exec.spi.ParameterBinder { private final Serializable entityId; private final JdbcMapping jdbcMapping; public EntityIdParameterBinder(Object entity, SharedSessionContractImplementor session) { this.entityId = session.getEntityPersister(null, entity).getIdentifier(entity, session); this.jdbcMapping = session.getEntityPersister(null, entity).getIdentifierMapping().getSingleJdbcMapping(); } @Override public void bind(PreparedStatement statement, int startPosition, QueryParameterImplementor<?> queryParameter, SharedSessionContractImplementor session) throws SQLException { jdbcMapping.getJdbcValueBinder().bind(statement, entityId, startPosition, session); } @Override public void bind(PreparedStatement statement, String name, QueryParameterImplementor<?> queryParameter, SharedSessionContractImplementor session) throws SQLException { jdbcMapping.getJdbcValueBinder().bind(statement, entityId, name, session); } }
后续需通过TypeContributor注册该绑定逻辑,或在Spring配置中自定义SessionFactory时注入参数解析规则。
方案2:Spring Data JPA AOP切面拦截
通过AOP拦截Repository原生查询方法,自动将实体参数替换为ID:
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import java.io.Serializable; import java.lang.reflect.Method; import java.util.Arrays; @Aspect @Component @Transactional public class NativeQueryEntityParameterAspect { @PersistenceContext private EntityManager entityManager; @Around("execution(* com.yourpackage.repository..*(..)) && @annotation(query)") public Object interceptNativeQueries(ProceedingJoinPoint joinPoint, Query query) throws Throwable { if (!query.nativeQuery()) { return joinPoint.proceed(); } Method method = ((org.aspectj.lang.reflect.MethodSignature) joinPoint.getSignature()).getMethod(); Object[] args = joinPoint.getArgs(); for (int i = 0; i < args.length; i++) { Object arg = args[i]; if (arg != null && entityManager.getEntityManagerFactory().getMetamodel().entity(arg.getClass()) != null) { Serializable id = entityManager.getEntityManagerFactory().getPersistenceUnitUtil().getIdentifier(arg); args[i] = id; } } return joinPoint.proceed(args); } }
方案3:逐步迁移(推荐长期方案)
上述方案仅为临时过渡,Hibernate 6.x的设计理念是强制显式参数绑定,避免隐式行为的潜在风险。长期来看,建议逐步将所有原生查询中的实体参数替换为对应ID值,确保代码符合新版本规范,避免后续兼容性问题。
内容的提问来源于stack exchange,提问作者The MW
相关产品推荐
相关产品推荐

