分类: Java
2014-08-12 14:41:29
Hibernate 是一个开放源代码的对象关系映射框架,它对 JDBC 进行了非常轻量级的对象封装,使得 Java 程序员可以随心所欲的使用面向对象编程思维来操纵数据库。Hibernate 的优势在于屏蔽了数据库细节,对于新增修改删除的数据层操作,不再需要跟具体的 SQL 语句打交道,简单的对对象实例进行增删改操作即可。
但是,对于多表关联、分组统计、排序等复杂的查询功能时,由于 Hibernate 自身的 O-R 映射机制,父子表之间关联取数据会产生大量冗余的查询操作,性能低下。此类情况下,直接使用 JDBC 的 SQL 语句反而更加灵活和高效。
考虑如下数据库实体示例,表 A 为主表,表 B 和表 C 为子表,A 与 B、A 与 C 表均为 1 对多关系,在 B 表和 C 表中以 A_ID 外键字段关联 A 表父记录。
图 1. 数据库实体示例图在 Hibernate 框架中,通常采用以下配置方式完成 A 表与 B,C 表父子实体之间的级联查询操作,Hibernate 实体配置 xml 如下:
清单 1. hibernate 实体配置 xmlA.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 otherAmount =otherSubject.getChildren_b().size() + otherSubject.getChildren_c().size(); if(curAmount otherAmount) { 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
表面看来很方便,但是由于 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 操作 sqlselect 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
在以上 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 之上的数据持久层实现,因此,要在框架本身提供操作 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
使用该解决方案时,只需要将代码包解压至项目源代码目录,在想要直接操作 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 的功能。
描述 | 名字 | 大小 |
---|---|---|
本文源代码 | code.zip | 3 KB |