如何在Hibernate Criteria查询中使用Oracle数组绑定(array bind)参数
好问题!针对你遇到的Oracle超长IN列表+Hibernate Criteria的痛点,确实可以通过Oracle的**数组绑定(array bind)**来解决,而且完全不需要放弃Criteria API。下面一步步给你讲怎么落地:
第一步:在Oracle数据库创建自定义数组类型
首先需要让DBA帮你在数据库里创建一个对应ID类型的数组(比如你的ID是数字型):
CREATE OR REPLACE TYPE NUMBER_ARRAY AS TABLE OF NUMBER;
如果是字符串ID,就改成VARCHAR2_ARRAY AS TABLE OF VARCHAR2(100)(长度根据你的ID实际情况调整)。
第二步:给Hibernate注册自定义数组类型
Hibernate默认不认识Oracle的自定义数组,所以需要写一个自定义类型来处理Java列表和Oracle数组的转换,同时注册到Hibernate中。
1. 实现自定义Hibernate类型
这里以Long类型ID为例,写一个OracleLongArrayType:
import org.hibernate.type.AbstractSingleColumnStandardBasicType;
import org.hibernate.type.descriptor.java.AbstractTypeDescriptor;
import org.hibernate.type.descriptor.sql.ArraySqlTypeDescriptor;
import java.sql.Array;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import oracle.jdbc.OracleConnection;
public class OracleLongArrayType extends AbstractSingleColumnStandardBasicType<List<Long>> {
public OracleLongArrayType() {
super(ArraySqlTypeDescriptor.INSTANCE, new LongArrayJavaTypeDescriptor());
}
@Override
public String getName() {
return "oracle-long-array";
}
public static class LongArrayJavaTypeDescriptor extends AbstractTypeDescriptor<List<Long>> {
public LongArrayJavaTypeDescriptor() {
super(List.class, new MutabilityPlan<List<Long>>() {
@Override
public List<Long> deepCopy(List<Long> value) {
return value != null ? new ArrayList<>(value) : null;
}
@Override
public boolean isMutable() {
return true;
}
});
}
@Override
public String toString(List<Long> value) {
return value != null ? value.toString() : null;
}
@Override
public List<Long> fromString(String string) {
throw new UnsupportedOperationException("直接字符串转列表暂不支持");
}
@Override
public <X> X unwrap(List<Long> value, Class<X> type, WrapperOptions options) {
if (value == null) {
return null;
}
if (Array.class.isAssignableFrom(type)) {
try {
Connection conn = options.getSession().doReturningWork(Connection::get);
OracleConnection oracleConn = conn.unwrap(OracleConnection.class);
return (X) oracleConn.createARRAY("NUMBER_ARRAY", value.toArray());
} catch (SQLException e) {
throw new RuntimeException("转换Oracle数组失败", e);
}
}
throw unknownUnwrap(type);
}
@Override
public <X> List<Long> wrap(X value, WrapperOptions options) {
if (value == null) {
return null;
}
if (value instanceof Array) {
try {
Object[] array = (Object[]) ((Array) value).getArray();
return Arrays.stream(array).map(Long.class::cast).collect(Collectors.toList());
} catch (SQLException e) {
throw new RuntimeException("转换Java列表失败", e);
}
}
throw unknownWrap(value.getClass());
}
}
}
如果是字符串ID,只要把Long换成String,数组类型名改成VARCHAR2_ARRAY即可。
2. 注册自定义类型
可以用注解或者XML配置,推荐在实体类或者专门的配置类上用@TypeDef:
import org.hibernate.annotations.TypeDef;
import javax.persistence.Entity;
@Entity
@TypeDef(name = "oracle-long-array", typeClass = OracleLongArrayType.class)
public class YourEntity {
// 你的实体字段...
}
或者在hibernate.cfg.xml里加:
<typedef name="oracle-long-array" class="com.yourpackage.OracleLongArrayType"/>
第三步:在Criteria查询中使用数组绑定
现在可以抛弃原来拆分IN子句的写法,改用SQLRestriction来关联数组参数:
if (idList) {
criteria.add(
Restrictions.sqlRestriction(
"id IN (SELECT column_value FROM TABLE(?))",
idList,
sessionFactory.getTypeHelper().custom(OracleLongArrayType.class)
)
)
}
如果已经通过@TypeDef注册了类型名,也可以直接用类型名简化:
criteria.add(
Restrictions.sqlRestriction(
"id IN (SELECT column_value FROM TABLE(?))",
idList,
org.hibernate.type.TypeHelper.custom("oracle-long-array")
)
)
这段SQL里的TABLE(?)是Oracle的语法,会把传入的数组转换成临时表,然后用column_value取数组中的每个元素,效果和原来的IN子句完全一致,但只有1个绑定参数,完美解决DBA提到的执行计划不稳定问题。
关键注意事项
- 确保数据库用户有访问自定义数组类型的权限,让DBA帮忙确认。
- 如果是Hibernate 4及以下版本,自定义类型需要实现
UserType接口(逻辑类似,只是写法略有不同)。 - 测试时对比性能:这种方式和原来拆分IN子句的性能应该相当,但胜在执行计划稳定,DBA更容易调优。
- 如果ID类型是字符串,一定要对应修改数组类型和自定义类型的处理逻辑,避免类型转换错误。
内容的提问来源于stack exchange,提问作者Kaypro II




