我们最近发布了一篇文章,介绍如何在SQL / JDBC和jOOQ中正确绑定Oracle DATE
类型 。 这篇文章在Reddit上颇受关注, Vlad Mihalcea对此发表了有趣的评论,他经常在其博客上撰写有关Hibernate,JPA,事务管理和连接池的博客 。 Vlad指出,使用Hibernate也可以解决此问题,我们很快将对此进行研究。
Oracle DATE有什么问题?
上一篇文章中提出的问题涉及以下事实:查询在Oracle DATE
列上使用过滤器:
// execute_at is of type DATE and there's an index
PreparedStatement stmt = connection.prepareStatement("SELECT * " + "FROM rentals " +"WHERE rental_date > ? AND rental_date < ?");
…,我们使用java.sql.Timestamp
作为绑定值:
stmt.setTimestamp(1, start);
stmt.setTimestamp(2, end);
…那么,即使我们应该进行常规的INDEX RANGE SCAN,执行计划对FULL TABLE SCAN还是INDEX FULL SCAN都会变得非常糟糕。
-------------------------------------
| Id | Operation | Name |
-------------------------------------
| 0 | SELECT STATEMENT | |
|* 1 | FILTER | |
|* 2 | TABLE ACCESS FULL| RENTAL |
-------------------------------------Predicate Information (identified by operation id):
---------------------------------------------------1 - filter(:1<=:2)2 - filter((INTERNAL_FUNCTION("RENTAL_DATE")>=:1 AND INTERNAL_FUNCTION("RENTAL_DATE")<=:2))
这是因为通过此INTERNAL_FUNCTION()
将数据库列从Oracle DATE
扩展到Oracle TIMESTAMP
,而不是将java.sql.Timestamp
值截断为Oracle DATE
。
有关问题本身的更多详细信息,请参见上一篇文章。
使用Hibernate防止此INTERNAL_FUNCTION()
您可以使用org.hibernate.usertype.UserType
通过Hibernate的专有API进行修复。
假设我们具有以下实体:
@Entity
public class Rental {@Id@Column(name = "rental_id")public Long rentalId;@Column(name = "rental_date")public Timestamp rentalDate;
}
现在,让我们在这里运行此查询(例如,我使用的是Hibernate API,而不是JPA):
List<Rental> rentals =
session.createQuery("from Rental r where r.rentalDate between :from and :to").setParameter("from", Timestamp.valueOf("2000-01-01 00:00:00.0")).setParameter("to", Timestamp.valueOf("2000-10-01 00:00:00.0")).list();
我们现在得到的执行计划再次效率低下:
-------------------------------------
| Id | Operation | Name |
-------------------------------------
| 0 | SELECT STATEMENT | |
|* 1 | FILTER | |
|* 2 | TABLE ACCESS FULL| RENTAL |
-------------------------------------Predicate Information (identified by operation id):
---------------------------------------------------1 - filter(:1<=:2)2 - filter((INTERNAL_FUNCTION("RENTAL0_"."RENTAL_DATE")>=:1 AND INTERNAL_FUNCTION("RENTAL0_"."RENTAL_DATE")<=:2))
解决方案是将此@Type
批注添加到所有相关列中…
@Entity
@TypeDefs(value = @TypeDef(name = "oracle_date", typeClass = OracleDate.class)
)
public class Rental {@Id@Column(name = "rental_id")public Long rentalId;@Column(name = "rental_date")@Type(type = "oracle_date")public Timestamp rentalDate;
}
并注册以下简化的UserType
:
import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.Objects;import oracle.sql.DATE;import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.usertype.UserType;public class OracleDate implements UserType {@Overridepublic int[] sqlTypes() {return new int[] { Types.TIMESTAMP };}@Overridepublic Class<?> returnedClass() {return Timestamp.class;}@Overridepublic Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner)throws SQLException {return rs.getTimestamp(names[0]);}@Overridepublic void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session)throws SQLException {// The magic is here: oracle.sql.DATE!st.setObject(index, new DATE(value));}// The other method implementations are omitted
}
这将起作用,因为使用供应商特定的oracle.sql.DATE
类型将对您的执行计划产生与在SQL语句中显式强制转换绑定变量相同的效果,如上一篇文章 CAST(? AS DATE)
。 现在,执行计划是所需的计划:
------------------------------------------------------
| Id | Operation | Name |
------------------------------------------------------
| 0 | SELECT STATEMENT | |
|* 1 | FILTER | |
| 2 | TABLE ACCESS BY INDEX ROWID| RENTAL |
|* 3 | INDEX RANGE SCAN | IDX_RENTAL_UQ |
------------------------------------------------------Predicate Information (identified by operation id):
---------------------------------------------------1 - filter(:1<=:2)3 - access("RENTAL0_"."RENTAL_DATE">=:1 AND "RENTAL0_"."RENTAL_DATE"<=:2)
如果您想重现此问题,只需通过JPA / Hibernate使用java.sql.Timestamp
绑定值查询任何Oracle DATE
列, 并按照此处所示获取执行计划 。
不要忘记刷新共享池和缓冲区高速缓存以在两次执行之间强制执行新计划的计算,因为每次生成的SQL都是相同的。
我可以使用JPA 2.1吗?
乍一看,看起来JPA 2.1中的新转换器功能( 就像jOOQ的转换器功能一样 )应该可以解决问题。 我们应该能够写:
import java.sql.Timestamp;import javax.persistence.AttributeConverter;
import javax.persistence.Converter;import oracle.sql.DATE;@Converter
public class OracleDateConverter
implements AttributeConverter<Timestamp, DATE>{@Overridepublic DATE convertToDatabaseColumn(Timestamp attribute) {return attribute == null ? null : new DATE(attribute);}@Overridepublic Timestamp convertToEntityAttribute(DATE dbData) {return dbData == null ? null : dbData.timestampValue();}
}
然后可以将此转换器与我们的实体一起使用:
import java.sql.Timestamp;import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.Id;@Entity
public class Rental {@Id@Column(name = "rental_id")public Long rentalId;@Column(name = "rental_date")@Convert(converter = OracleDateConverter.class)public Timestamp rentalDate;
}
但是不幸的是,这并不是开箱即用的,因为Hibernate 4.3.7会认为您将要绑定VARBINARY
类型的变量:
// From org.hibernate.type.descriptor.sql.SqlTypeDescriptorRegistrypublic <X> ValueBinder<X> getBinder(JavaTypeDescriptor<X> javaTypeDescriptor) {if ( Serializable.class.isAssignableFrom( javaTypeDescriptor.getJavaTypeClass() ) ) {return VarbinaryTypeDescriptor.INSTANCE.getBinder( javaTypeDescriptor );}return new BasicBinder<X>( javaTypeDescriptor, this ) {@Overrideprotected void doBind(PreparedStatement st, X value, int index, WrapperOptions options)throws SQLException {st.setObject( index, value, jdbcTypeCode );}};}
当然,我们可能可以通过某种方式调整此SqlTypeDescriptorRegistry
来创建自己的“绑定程序”,但随后我们将返回特定于Hibernate的API。 这个特定的实现可能是在Hibernate端的一个“ bug”,已在此处注册以作记录:
https://hibernate.atlassian.net/browse/HHH-9553
结论
即使抽象被JCP视为“标准”,抽象在各个级别上都是泄漏的。 标准通常是事后证明行业实际标准的一种手段(当然要涉及一些政治因素)。 我们不要忘记,Hibernate并不是从一个标准开始的,而是在14年前彻底改变了标准的J2EE人士对持久性的思考方式。
在这种情况下,我们有:
- Oracle SQL的实际实现
- SQL标准,它指定的
DATE
与Oracle完全不同 - ojdbc,它扩展了JDBC以允许访问Oracle功能
- JDBC,在时间类型方面遵循SQL标准
- Hibernate,它提供专有的API,以便在绑定变量时访问Oracle SQL和ojdbc功能
- JPA,它在时间类型方面再次遵循SQL标准和JDBC
- 您的实体模型
如您所见,实际的实现(Oracle SQL)通过Hibernate的UserType
或JPA的Converter
泄漏到您自己的实体模型中。 从那时起,它将有望与您的应用程序隔离开来(直到不会),使您无需理会这个讨厌的Oracle SQL详细信息。
无论如何,如果您想解决实际的客户问题(即即将出现的重大性能问题),那么您将需要使用Oracle SQL,ojdbc和Hibernate的特定于供应商的API –而不是假装该SQL ,JDBC和JPA标准是最基本的要求。
但这可能没关系。 对于大多数项目,最终的实现锁定是完全可以接受的。
翻译自: https://www.javacodegeeks.com/2015/01/leaky-abstractions-or-how-to-bind-oracle-date-correctly-with-hibernate.html