hibernate连接泄露_泄漏抽象,或如何正确地与Hibernate绑定Oracle DATE

hibernate连接泄露

我们最近发布了一篇文章,介绍如何在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 {

    @Override
    public int[] sqlTypes() {
        return new int[] { Types.TIMESTAMP };
    }

    @Override
    public Class<?> returnedClass() {
        return Timestamp.class;
    }

    @Override
    public Object nullSafeGet(
        ResultSet rs, 
        String[] names, 
        SessionImplementor session, 
        Object owner
    )
    throws SQLException {
        return rs.getTimestamp(names[0]);
    }

    @Override
    public 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>{

    @Override
    public DATE convertToDatabaseColumn(Timestamp attribute) {
        return attribute == null ? null : new DATE(attribute);
    }

    @Override
    public 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.SqlTypeDescriptorRegistry

    public <X> ValueBinder<X> getBinder(JavaTypeDescriptor<X> javaTypeDescriptor) {
        if ( Serializable.class.isAssignableFrom( javaTypeDescriptor.getJavaTypeClass() ) ) {
            return VarbinaryTypeDescriptor.INSTANCE.getBinder( javaTypeDescriptor );
        }

        return new BasicBinder<X>( javaTypeDescriptor, this ) {
            @Override
            protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options)
                    throws SQLException {
                st.setObject( index, value, jdbcTypeCode );
            }
        };
    }

当然,我们可能可以通过某种方式调整此SqlTypeDescriptorRegistry来创建自己的“绑定程序”,但随后我们将返回特定于Hibernate的API。 这个特定的实现可能是Hibernate端的“错误”,已在此处注册以作记录:

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

hibernate连接泄露

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值