在Hibernate中直接操作JDBC接口

简介: Hibernate 在处理多表关联及分组排序等复杂数据库查询操作时,其固有的 O-R 映射机制会 产生大量冗余 SQL 操作,系统性能比传统的 JDBC 低很多。本文分析了 Hibernate 产生此类问题的原因 ,提出了一个在 Hibernate 框架内直接操作 JDBC 的接口的解决方案,在实际项目中验证了该解决方案 可以有效提高此类查询的效率。文中提供的示例代码可以直接运用于使用 Hibernate 框架的 J2EE 系统 项目。

在 Hibernate 框架中提供直接操作 JDBC 接口的原因

Hibernate 框架在处理复杂查询方面的 问题

Hibernate 是一个开放源代码的对象关系映射框架,它对 JDBC 进行了非常轻量级的对象封 装,使得 Java 程序员可以随心所欲的使用面向对象编程思维来操纵数据库。Hibernate 的优势在于屏蔽 了数据库细节,对于新增修改删除的数据层操作,不再需要跟具体的 SQL 语句打交道,简单的对对象实 例进行增删改操作即可。

但是,对于多表关联、分组统计、排序等复杂的查询功能时,由于 Hibernate 自身的 O-R 映射机制 ,父子表之间关联取数据会产生大量冗余的查询操作,性能低下。此类情况下,直接使用 JDBC 的 SQL 语句反而更加灵活和高效。

Hibernate 框架处理复杂查询问题实例分析

考虑如下数据库实 体示例,表 A 为主表,表 B 和表 C 为子表,A 与 B、A 与 C 表均为 1 对多关系,在 B 表和 C 表中 以 A_ID 外键字段关联 A 表父记录。

图 1. 数据库实体示例图

在 Hibernate 框架中,通常采用以下配置方式完成 A 表与 B,C 表父子实体之间的级联查询操作, Hibernate 实体配置 xml 如下:

清单 1. hibernate 实体配置 xml

A.hbm.xml:                                                                           B.hbm.xml:                                                     C.hbm.xml                                                      

对应 的 Hibernate 领域实体类代码示例如下:

清单 2. hibernate 实体类示例

A.java:public class A implements java.io.Serializable,Comparable  {  private long id;  private Set children_b = new HashSet ();  private Set children_c = new HashSet();  public A (long id) {    this.id = id;  }  public long getId()  {    return id;  }  public void setId(long id) {    this.id = id;  }  public Set getChildern_b() {     return children_b;  }  public void setChildren_b (Set  children_b) {    this.children_b = children_b;  }  public  Set getChildern_c() {    return children_c;  }  public  void setChildren_c (Set children_c) {    this.children_c = children_c;  }  public int compareTo(Object other) {    A otherSubject  = (A)other;    long curAmount=this.getChildren_b().size()+this.getChildren_c ().size();    long therAmount =otherSubject.getChildren_b().size()   +  otherSubject.getChildren_c().size();    if(curAmountotherAmount)    {      return 1;    }     else    {      return 0;    }  }}B.java:public class B implements java.io.Serializable,Comparable {  private long id;  private long a_id;  public long getId()  {    return id;  }  public void setId(long id) {    this.id = id;  }  public long getA_id() {     return a_id;  }  public void setA_id(long a_id) {     this.a_id = a_id;  }  public B(long id) {     this.id=id;  }}C.java:public class C implements  java.io.Serializable,Comparable {  private long id;  private long  a_id;  public long getId() {    return id;  }  public void setId(long id) {    this.id = id;  }   public long getA_id() {    return a_id;  }  public  void setA_id(long a_id) {    this.a_id = a_id;  }   public C(long id) {    this.id=id;  }}

假设现在要统 计 A 表中从属的 B 表和 C 表记录之和最高的 top10 的 A 表记录,在 Hibernate 框架下,由于取 A 表对应的数据库记录时,已关联取出了对应的 B、C 表子记录存放于 A 实体类的 children_a, children_c 的属性中,因此 top10 的功能可以通过比较每个 A 表实体类中 children_a、children_c 的 Set 的 size 大小并进行排序得到,其代码示例如下:

清单 3. 排序代码示例

private ArrayList sortAByAmount(ArrayList all){  for(int i=0;i  {    for(int j=0;j    {      if(all.get(j).compareTo(all.get(j+1))<=0)      {        A temp = all.get(j);        all.set (j,all.get(j+1));        all.set(j+1,temp);      }    }   }  return all;}

表面看来很方便,但是由于 Hibernate 是 面向对象的 O-R 映射机制,每一条 A 表记录的查询实际都关联有两条 B、C 表查询的 SQL 产生,我们 可以看到 Hibernate 的 SQL 日志中:

清单 4. Hibernate sql 日志示例

Hibernate: select a0_.ID as ID2_ from A a0_ where a0_.ID='1' Hibernate: select b0_.ID as ID2_,b0_.A_ID as A_ID2_ from B b0_ where b0_.ID=?  Hibernate: select c0_.ID as ID2_,c0_.A_ID as A_ID2_ from C c0_ where  c0_.ID=?

由上述 Sql 日志可以看出,每一条 A 表记录的取出,都伴随以 A 表 ID 为查 询条件关联 B,C 表中 A_ID 外键字段的 2 条取子记录的 sql,这是由 A.hbm.xml 配置中 “lazy=false”决定的。

这种情况下,当 A 和 B、C 表中数据量越来越大时,A 表取 实体的操作开销将随着 sql 查询的增多而增大,并且在紧接着的排序过程中,即使采用业界最快的快速 排序算法,排序时间依然是随原始排序实体数量的线性关系(O(n lg n)),效率会线性下降,最终无法 满足客户的前台查询的效率要求。

此类情况下如直接采用 JDBC,则只需一条如下的 SQL 语句, 即可完成该功能:

清单 5. 直接 JDBC 操作 sql

select tab1.ID,tab1.sumCol+tab2.sumColfrom (select a.ID,count(b.ID) sumColfrom A a left join B b on a.ID=b.IDGROUP BYa.ID)tab1,(select a.ID,count(c.ID) sumColfrom A a left join C c on a.ID=c.IDGROUP BYa.ID)tab2 where tab1.ID=tab2.IDorder by  tab1.sumCol+tab2.sumCol desc

在以上 JDBC 方式下,即使 A、B、C 表的数据量持续增 长,仍然只有 1 条 SQL 的开销,不会出现 SQL 递增的情况,因此耗时是在可控制的区间内的。并且读 者可以注意到上述 SQL 将 3 表关联拆分成了 2 个子查询,这样避免了 3 表做笛卡尔积的数量和,进一 步提高了查询效率。由此可见,直接操作 JDBC,除 SQL 的开销可控外,还可以利用数据库层各种机制, 如上述查询语句中的 left join、子查询、索引…,灵活的调整 SQL 语句,以达到最佳的查询性 能。

由上可实例可看出,在多表关联、排序等复杂的查询情况下,Hibernate 框架由于其自身对 象封装的特殊性,不能像 JDBC 直接操作 SQL 那样很好的解决查询中高效性和灵活性方面的需求,且由 于其屏蔽了数据库的底层,开发人员看到的只是 Hibernate 提供的数据层 API,无法与灵活的使用 SQL 语句等数据库底层细节。因此,有必要在 Hibernate 框架中提供直接操作 JDBC 的接口。

在 Hibernate 框架中提供操作 JDBC 的接口的解决方案

Hibernate 的 session 机制

我们知 道 Hibernate 框架本身也是建立在 JDBC 之上的数据持久层实现,因此,要在框架本身提供操作 JDBC 的接口,需要切入其对 JDBC 封装的细节。

通过研究和查阅 Hibernate 的框架源代码及参考文档 ,我们发现,Hibernate 的 Session 会话是进行持久化的基础,所有的持久化操作都是在 Session 的基 础上进行的,在实现上它是和 JDBC 中的 connection 数据库连接绑定的,也就是说,Hibernate 的会话 域基于一个实际的 connection 类实例,二者之间的关系如下图所示:

图 2. Hibernate Session 机制示意图

由上可以看到, Hibernate 中的 session 是单线程的,代表了一次会话的过程。实际上是把一个 JDBC Connection 打包 了,每一个 Session 实例和一个数据库事务绑定。其生命周期是与与之关联的 connection 实例的生命 周期一致的。

具体解决方案

由上面的 Hibernate 的 Session 机制我们意识到,只要能获 取到 Hibernate 当前会话中的 Connection,则获得了 JDBC 的底层数据库连接实例,剩下就都是 JDBC 的范畴了。再查阅 Hibernate 的 API,发现 HibernateTemplate 类中 SessionFacTory 成员的 getCurrentSession() 方法即可获得 Hibernate 环境下的当前活动的 Session 会话,而 Hibernate 中 Session 实例的 connection() 方法即可获得该会话中绑定的 Connection 数据库连接实例。

问 题迎刃而解了,既然可以操作 Connection 实例,那与之关联的 Statement、ResultSet 等基本 JDBC 类 均在我们控制范围中了,我们采用接口模式设计一个轻量级解决方案,使其在保持原 Hibernate 的增删 改操作方式前提下灵活提供操作 JDBC 的接口。设计类图如下图所示:

图 3. 解决方案设计类示 意图

设计中, AbstractHibernateDao 类作为 DAO 操作的基本类,保留原有 Hibenrate 框架下的新增,修改,删除等 API。BaseHibernateDao 类继承 AbstractHibernateDao 类,在此类中增加了直接操作 JDBC 的接口。设 计 getConnection 方法获取 JDBC 的数据库连接实例,设计 getObjectsBySql 方法作为对外的主要接口 ,该方法调用 fetchObjects 方法,这是具体的数据库记录到领域对象的转换操作,需要使用者 override 该方法以完成自有领域对象的填充细节。

实际实现的类代码如下所示:

清单 6. 解决方案实现代码

AbstractHibernateDao.java:abstract public class  AbstractHibernateDao extends HibernateDaoSupport {  protected Log logger  = LogFacTory.getLog(getClass());  protected Class entityClass;   protected Class getEntityClass() {    return entityClass;  }  public List getAll() {    return getHibernateTemplate().loadAll (getEntityClass());  }  public void save(Object o) {     getHibernateTemplate().saveOrUpdate(o);  }  public void removeById (Serializable id) {    remove(get(id));  }  public void  remove(Object o) {    getHibernateTemplate().delete(o);  }} BaseHibernateDao.java:abstract public class BaseHibernateDao extends  AbstractHibernateDao{  public Connection getConnection()  {     try    {      Session curSeesion =null;       Connection con =null;      curSeesion = super.getHibernateTemplate ().getSessionFacTory()     .getCurrentSession();      con =  curSeesion.connection();      return con;    }    catch (Exception es)    {      System.out.println(es.getMessage());       return null;    }  }  public  ArrayList fetchObjects(ResultSet rs)  {     ArrayList ret = new ArrayList();    //example:    //while(rs.next())    //{    //Object bject = new Object ();    //rs.getString(1);    //rs.getString(2);    //ret.add (object);    //}    return ret;  }  public  ArrayList getObjectsBySql(String pureSql)  {    Connection  con = curSeesion.connection();    ps  =  con.prepareStatement (sqlbuf.toString());    rs = ps.executeQuery();    try     {      return this.fetchObjects(rs);    }    catch (Exception es)    {      System.out.println(es.getMessage());       return null;    }    finally    {       try      {        ps.close();        rs.close();        con.close();      }      catch (SQLException e)  {        // TODO Auto-generated catch block         e.printStackTrace();      }    }  }}

使用该解决方案时,只需要将代码包解压至项目源代码目录,在想要直接操作 JDBC 接口的 DAO 模块继 承 BaseHibernateDao 类,然后重写 fetchObjects 方法填入从自身数据库表字段填充到领域对象的操作 , 即可轻松调用 getObjectsBySql 传入原生 SQL 语句,返回具体的领域对象实体集合,当然使用者也可 以通过 getConnection 获得 JDBC 的 Connection 实例来进行自己需要的特定的 JDBC 底层操作。

仍然以上文中的 A、B、C 表为例,采用该解决方案完成 top10 取数的代码示例如下:

清 单 7. 使用解决方案示例

public class testDAO extends BaseHibernateDao{  private String sqlQuery = " select tab1.ID,tab1.sumCol+tab2.sumCol"+    " from(select a.ID, count(b.ID) sumCol"+    "   from A a left  join B b on a.ID=b.ID"+    "   GROUP BY a.ID)tab1, "+    "    (select a.ID,count(c.ID) sumCol"+    "   from A a left join C c on  a.ID=c.ID"+    "   GROUP BY a.ID)tab2"+    " where  tab1.ID=tab2.ID"+    " order by tab1.sumCol+tab2.sumCol desc";   @override  public ArrayList fetchObjects(ResultSet rs)  {    ArrayList ret = new ArrayList();    int count=1;    while(rs.next())    {      A a = new A();       a.setId(rs.getLong(1));      System.out.println("top"+(count++)+"  amount:"+rs.getLong(2));      ret.add(object);    }     return ret;  }}

解决方案验证

在实际 mySql 数据库环境 中,以 A 表数据量 1000 条,B 表数据量 3W 多条,C 表数据量 2000 条情况下进行上文中提到的 top10 的操作,采用 Hibernate 的耗时和用 JDBC 接口解决方案的效率比较如下:

表 1. Hibernate 框架方式与 JDBC 接口方式效率比较 1:

  Hibernate 框架方式 采用 JDBC 接口解决方案 查询耗时 1475ms 1096ms 排序耗时 1035ms 0ms 总计: 2110ms 1096ms

A 表数据量 2000 条,B 表数据量 6W,C 表数据量 4000 条情况下,采用 Hibernate 的耗时和用 JDBC 接口解决方 案的效率比较:

表 2. Hibernate 框架方式与 JDBC 接口方式效率比较 2:

  Hibernate 框架方式 采用 JDBC 接口解决方案 查询耗时 2836ms 1657ms 排序耗时 1568ms 0ms 总计: 4404ms 1657ms

由以上结果可以看出:在数据量递增的情况下,采用 Hibernate 方式下效率 与库表数据呈线性增长,且排序的操作的效率也是一样,而直接采用 JDBC 接口解决方案下效率远远高于 Hibernate 方式,且在数据量增长的情况下耗时的增长速度处于合理的区间内。

总结

本文 分析了 Hibernate 框架在处理复杂查询功能上的效率问题,提出并实现了一个在 Hibernate 框架内提供 直接 JDBC 操作接口的解决方案,并实际验证了该解决方案的有效性,文中的源代码可以直接运用于选择 Hibenrate 框架作为数据持久层实现的 J2EE 项目,使之具备操作底层 JDBC 的功能。

本文配套源码

其实只要你愿意,一切都可以变得很容易。

在Hibernate中直接操作JDBC接口

相关文章:

你感兴趣的文章:

标签云: