面向对象的 DB2 pureXML 应用程序开发
 

2009-10-21 作者:黄耀华,李玉明 来源:IBM

 
本文内容包括:
本文介绍了如何使用 Hibernate 来简化基于 DB2 pureXML 的应用程序的开发,力图通过若干个例子帮助读者映射 DB2 pureXML 的数据,减少开发难度。

前提

读者需要对 Hibernate 框架和 DB2 pureXML 相关技术,如 SQL/XML,XQuery 等有一定的了解。本文所述基于 DB2 V9.5 和 Hibernate 3.3.1GA 。由于 DB2 V9.7 的 Sample 数据库中示例 XML 表中不再包含命名空间,如果读者使用 DB2 V9.7,读者需要在示例代码中去掉对命名空间的声明。

Hibernate 简介

如今的企业级应用程序的开发基本都是以面向对象的方式进行的,Java 社区丰富的资源为开发过程提供了产品质量和开发效率的双重保障。然而,在数据库层面,由于各种原因,比如使用习惯,性能考虑等因素,却依旧沿袭了“实体 - 关系”的数据模型设计,将立体化的对象切割成为各个平面的表结构,然后再在这些表之间建立起来关联。另一方面来讲,数据库本身的发展也遇到了瓶颈,暂无很好的办法以直接的方式管理对象(本文指 Java 类的实例),提供基于对象的 CRUD( 增删改查 ) 等操作,以及事务控制等。

基于以上考虑,开源的 Hibernate 作为一个关系型数据库的对象映射框架,已经成为了企业级应用程序开发人员的首选。开发人员常常利用 Hibernate 作为粘合剂,把 Java 对象和关系型数据库粘合起来,免去手工映射的繁琐工作。

DB2 pureXML 的用武之地

Java 和 XML 可谓是天生的盟友。两者都可以用来描述复杂的对象,而且两者之间的转换也易如反掌。有许许多多的开源项目可以支持 Java 对象到 XML 的序列化 (serialization) 和 XML 文档到 Java 对象的反序列化 (de-serialization) 。 Java 利用 SAX 和 DOM 方式解析 XML 文档,并进行相应的操作,也是得心应手。

如果给 Java 设计人员两个选择,其一是拆分 Java 对象为若干个复杂的表,这些表间又有千丝万缕的联系,其二是直接将 Java 对象序列化成 XML,然后存入数据库,同时保证插入查询等数据库操作的性能;我想很少有人可以拒绝第二个选择。

DB2 pureXML 在这样的背景下可以发挥其独特的价值。无需将 XML 文档拆分成关系表,可直接插入到数据库中;查询时,既可以把整个文档从数据库中取出来,也可以只摘取 XML 文档的某若干个节点元素,还可以将某一个或多个元素作为查询条件。数据更新时,既可以将整个 XML 文档更新,也可以只更新文档的若干元素。这些操作都是基于业界标准的 XQuery 和 SQL/XML 。关于 DB2 pureXML 的数据操作接口和方式,读者可以参考相应文章,这里不再赘述。

这些特性无疑给了程序设计人员很大的想象空间和设计灵活性。

Hibernate 和 DB2 pureXML 如何共存

Hibernate 作为一个 ORM(Object-Relational Mapping) 工具,首先考虑的是对象(Object)和关系表(Relational)之间的映射关系。而 DB2 pureXML 所采用的 SQL/XML 和 XQuery 接口并没有在 Hibernate 中得到直接的支持。咋看起来,似乎为了使用 DB2 pureXML,只能

放弃 Hibernate 框架,改用 JDBC 来实现数据访问接口。

笔者经过一些探索,发现事实并非如此。在 Hibernate 3.3.1GA 版本中,已经可以对 DB2 pureXML 的绝大多数操作提供支持。只不过这些支持方式尚不为人熟悉罢了。本文的剩余部分将分门别类的就如何在 Hibernate 框架之上开发 DB2 pureXML 的应用程序详细介绍。

在阅读以下内容的同时,读者可以参考示例代码进行尝试。在开始之前,建议用户创建包含 XML 示例表的 sample 数据库。在 db2cmd 下运行 db2sampl – xml 即可。 XML 示例表将创建在当前用户 schema 下。

各种查询和对象映射方式

总体来讲,在 Java 语言中对 XML 文档的对象映射通常采用以下几种方式 :

  • 映射成为 DOM 对象 ;
  • 摘取 XML 文档中的部分元素 , 映射成为普通 Java 类 ;
  • 对复杂多层次的 XML 文档 , 映射成为若干个互相引用的 Java 类 ;
  • 将 XML 文档映射成为动态类的实例。

而在 Hibernate 里面,可以有如下更丰富的选择。当然,这些选择也只是对以上方式的扩展而已。

整体获取 XML 文档

Hibernate 支持将 XML 文档直接映射为 java.lang.String 类型。因此,如果希望获取整个 XML 文档,可以采用这种方式将 XML 文档返回为 Java 的 String 对象,然后在 Java 程序中使用 JDOM 等工具对该对象做进一步处理。这种处理方式和对 CLOB 数据类型的处理方式类似。

另外,Hibernate 允许用户自定义数据类型,由该数据类型和数据库中 XML 列直接交互。读者可以参考https://www.hibernate.org/466.html,该篇文章介绍了如何编写自定义用户类型来将 DB2 中的 XML 列映射成为 DOM 对象。

原生 SQL 查询

Hibernate 常用的查询语言是 HQL,但是也支持数据库原生 SQL 。用户可以直接编写原生 SQL 来支持具有数据库独有特性的 SQL 。如清单 1 所示,用户编写 SQL/XML,读取 /customerinfo/name 节点和 /customerinfo 下的 Cid 属性。

清单 1. 查询 Cid 代码
 
SQLQuery query2 = session 
	 .createSQLQuery("select xmlquery('$info/*:customerinfo/*:name/text()' 
	 passing info as \"info\") as name, 
 xmlcast(xmlquery('$info/*:customerinfo/data(@Cid)' 
 passing info as \"info\") as integer) as Cid from customer c");

Hibernate 将返回结果作为标量数组处理,在该例中,Cid 被映射为 Integer 类型,name 映射为 String 类型,如清单 2 所示。

清单 2. 映射变量类型
 
query2 = query2 
	 .addScalar("Cid",Hibernate.INTEGER) 
	 .addScalar("name",Hibernate.STRING); 
 List list = query2.list(); 
 for(inti = 0; i < list.size(); i++) { 
 Object[] objects = (Object[])list.get(i); 
 System.out.print("Cid:"+ objects[0] + "\t"); // 取出 Cid 
 System.out.println("name:"+ objects[1]); // 取出 name 
 }

完整的例子,读者可以参考 eg.hibernate.eg2.Test2 。

当然,如果读者已经有一个类 TestEntity 来封装 Cid 和 name,那么读者可以将返回结果映射成为该类的实例列表,以方便后续处理。

清单 3. 返回结果
 
SQLQuery query3 = session 
           .createSQLQuery("select xmlcast(xmlquery('$info/*:customerinfo/data(@Cid)' 
		 passing info as \"info\") as integer) as {entity.Cid}, 
 xmlquery('$info/*:customerinfo/*:name/text()' 
		 passing info as \"info\") as {entity.name} 
 from customer c"); 
 query3.addEntity("entity", TestEntity.class); 
 List list = query3.list();

注意观察以上语句可以发现,除了 addScalar() 被换成 addEntity() 以外,createSQLQuery 的参数也有少许变化。大括号将 {entity.Cid} 和 {entity.name} 括起来,它告诉 Hibernate,“ entity ”是一个特殊的对象,需要 Hibernate 做类型映射。读者可以参考 eg.hibernate.eg3.Test3 中的完整例子。

命名查询

如果需要在各个类中多次进行类似的查询,显然上述的即时查询是不够优美的。因为查询多少次,就要重复写多少遍的 SQL/XML 语句。其中的一个解决办法可以是使用 Hibernate 的命名查询。其语法如下:

清单 4. 使用 Hibernate 的命名查询

Query query4 = session.getNamedQuery("eg.hibernate.eg4.TestEntity4.testQuery"); 
 query4.list();

比起前面的例子,上面的代码显得整洁了许多,也更加符合责任分离(Separation of Duty)的原则。 Java 程序员只需要知道从某一个命名查询中可以取得什么样的结果即可,至于该命名查询代表什么样的查询语句,可以有负责数据的程序员在映射文件中提供:

清单 5. 命名查询映射文件
 
<sql-query name="testQuery"> <return class="eg.hibernate.eg4.TestEntity4"> </return> 
 select xmlcast(xmlquery('$info/*:customerinfo/data(@Cid)' 
           passing info as "info") as integer) as Cid2, 
	 xmlquery('$info/*:customerinfo/*:name/text()' 
		 passing info as "info") as name 
 from customer c 
 </sql-query>

在早期的版本中,Hibernate 对于映射文件中 sql-query 元素中的冒号的默认处理是作为绑定变量的占位符来处理,所以如果有 SQL/XML 查询含有命名空间,Hibernate 则不能正确处理。在 3.3.1GA 版本中,已经可以正确识别冒号,并将其当成普通的字符处理。而且,Hibernate 本身的可扩展性也允许用户扩展语法。读者可以参考 eg.hibernate.base.DB2V95Dialect 如何扩展默认的 DB2Dialect 来支持 xmlcast,xmlquery 等语法。

eg.hibernate.eg4.Test4 是完整的例子,示例了如何使用命名查询来查询 XML 数据。

使用 formula 映射只读对象

在上一节的例子中,查询被封闭在 sql-query 中,查询结果和对象的映射关系不是一目了然。 Hibernate 的 formula 提供了一种更直接的映射方式,允许将 Java 属性直接映射到 SQL/XML 查询片段。如下所示,name 属性映射到了对 /customerinfo/name/text() 的查询。

清单 6. formula 提供了一种更直接的映射方式
 
<property name="name"type="java.lang.String"> 
 <formula> 
 (xmlquery('$info/*:customerinfo/*:name/text()' 
 passing info as "info") ) 
 </formula> 
 </property>

Hibernate 在解析 HQL 的时候,HQL 中如果包含该属性,Hibernate 将会展开该属性的 formula 。

例如,对于 “ from TestEntity8 where name= ’ TOM ’” 这样的 HQL,展开后的 SQL 片段则为:

清单 7. 展开后的 SQL 片段
 
				
select … from Customer testentity0_ where 
 xmlquery('$info/*:customerinfo/*:name/text()' 
passing testentity0_.info as "info")='TOM'

需要注意的是,使用 formula 映射的属性是只读属性,这意味着不可以通过修改该属性值,来让 Hibernate 将其更新到数据库中,因而在实际场景中要慎重使用。读者可以参考 eg.hibernate.eg8.Test8 中完整的例子。关于 XML 更新,请参考后续小节的内容。

对象的 load

Hibernate 的一个重要特点是,用户无需编写繁琐的 JDBC 操作,然后新建一个目标类对象,将查询结果和目标对象的属性值进行绑定。在 Hibernate 中,只需要调用 session.load() 方法即可。在通常情况下,该方法会获取映射文件中的 ID 属性所对应的列,然后传递第二个参数作为该列的值进行查询,而后将查询结果绑定为第一个参数类的实例并返回。如下所示:

清单 8. 调用 session.load() 方法
 
				
TestEntity5 obj = (TestEntity5)session.load(TestEntity5.class,newInteger(1004));

如果用户没有定义 ID 列,比如,用户用 XML 文档中的某一个属性(/customerinfo/@Cid)来作为唯一标识。那么用户可以通过自定义的 loader 来实现。 Hibernate 将第二个参数的值传递给映射文件中 loader 对应的查询,然后就返回结果绑定为特定对象。

虽然可以这么做是可行的,但是笔者还是强烈建议在设计数据模型的时候将需要排序的值,主键,外键等信息放在关系列中,一方面查询起来比较方便,另一方面性能上相对也会好些。

清单 9. 通过自定义的 loader 来实现
 
<loader query-ref="eg.hibernate.eg5.TestEntity5.getById"/> 

 <sql-query name="getById"> 
<return alias="entity"class="eg.hibernate.eg5.TestEntity5"/> 
 select xmlcast(xmlquery( 
 'declare default element namespace "http://posample.org"; 
 $info/customerinfo/data(@Cid)' passing info as "info") as integer) 
 as {entity.Cid}, 
 xmlquery('$info/*:customerinfo/*:name/text()' passing info as "info") 
 as {entity.name} from customer c where 
 xmlexists('declare default element namespace "http://posample.org"; 
 $info/customerinfo[@Cid=$id]' 
 passing info as "info",cast(:id as int) as "id") 
 </sql-query>

 XML 节点引用外部字典表的查询

在某些特定场景下,为了节省存储空间, XML 某节点只保存了代码(如邮政编码),需要检索字典表(如行政区划表)才能拿到其真实值(如地区名称)。可以通过 formula 方式完成。由于在上 2 节已经介绍 formula 用法,这里不再赘述。详细例子请参考 eg.hibernate.eg8.Test8 中如何处理 lookup 属性。用户除了 sample 数据库中默认的 XML 表格,还需要新建字典表。详见附件中的 prereq.sql 。

多态和继承

面向对象重要的两个特征就是多态和继承。这两个特性充分体现了面向对象所带来的灵活性,借助 Hibernate,我们可以配合 XML 在数据模型的灵活性,达到强强联合的效果。

还是以 sample 数据库中的 customer 表为例,如图 1 所示,大多数 customer 信息包含了 name,addr 和 phone 信息。

图 1. customer XML 的普遍格式
图 1. customer XML 的普遍格式

然而,有些客户有额外的信息,如图 2 中 Cid 为 1005 的 Larry Menard 多了助手名称和电话。这正是 XML 在数据模型灵活性的体现。用户无需另建一张表,如助手信息表,来存储助手信息。

图 2. 包含助手信息的 customer XML 信息
图 2. 包含助手信息的 customer XML 信息

推而广之,随着业务变化,当业务数据的模型发生变化时,用户也无需对数据库结构进行修改,从而达到业务系统的不停机升级。 DB2 pureXML 对多版本 XML 数据的兼容性在这里起到了决定性的作用。

需要注意的是,数据模型的确定和修改需要慎之又慎,不可贪图方便,将所有信息都一股脑的放入 XML 中,并且放任 XML 结构的变化。久而久之,XML 内部结构则千奇百怪,完全失去控制,数据失去了其应有的意义。比如假设说现在要增加客户的银行交易信息,则不应该将客户银行账户,交易信息等全部放入原 XML 中。因为逻辑上讲,客户的交易信息,并不属于客户本身。比较好的方式是新建数据表存放交易信息,在客户 XML 中增加帐户信息节点。

话说回来,DB2 pureXML 给数据模型带来了很大的灵活性,也相应的给编程工作带来了一些难度。下面笔者将结合上述情形通过代码示例说明如何解决这个问题。

对于 customer 表,为了演示简单考虑,我们暂时只需要其 Cid 和 name 属性,则可以映射到类 TestEntity9:

清单 10. 映射到类 TestEntity9
 
public 
	classTestEntity9implementsSerializable{ 
	privateInteger Cid; 
	privateString name; … getters/setters 
 }

对于有助手的 customer,通过继承 TestEntity9,以 TestEntity9_V1 来描述他。

清单 11. 继承 TestEntity9
 
public 
	classTestEntity9_V1extendsTestEntity9 { 
	privateString assistantName; 
	privateString assistantPhone; … /getters/setters 
 }

映射文件中利用 discriminator 来告诉 Hibernate 如何区分返回结果集,映射到那个类。下面给出映射文件的代码片段,请在附件中查看完整代码。需要注意的重点是 :

  • class 增加了 discriminator-value,其值为 false
  • class 下增加了 discriminator,其 formula 为判断 /customerinfo/assistant 是否存在
  • class 下增加了 sub-class,名称为 eg.hibernate.eg9.TestEntity9_V1 。其 discriminator-value 为 true

由以上三点,我们大概可以猜出 Hibernate 在做什么工作。 Hibernate 首先计算 discriminator 的值,然后依据该值,和各个 class 和 sub-class 的 discriminator-value 做比对,如果匹配上,则生成该 class 的实例,并将结果集赋值到该实例中。

清单 12. 生成该 class 实例
 
				
<class name="eg.hibernate.eg9.TestEntity9"table="Customer"discriminator-value="false"> 
…
 <discriminator type="string"> 
 <formula> 
 (xmlcast(xmlquery( 
 'declare default element namespace "http://posample.org"; 
 $info/customerinfo/fn:exists(assistant)' passing info as "info") 
 as varchar(10))) 
 </formula> 
 </discriminator> 

 <subclass name="eg.hibernate.eg9.TestEntity9_V1"discriminator-value="true"> 
 <property name="assistantName"type="string"> <formula>(xmlquery( 
 '$info/*:customerinfo/*:assistant/*:name/text()' 
 passing info as "info") )</formula> </property> 
 <property name="assistantPhone"type="string"> <formula>(xmlquery( 
 '$info/*:customerinfo/*:assistant/*:phone/text()' 
 passing info as "info") )</formula> </property> 
 </subclass> 
 </class>

XML 数据的增删改

增加

如果 Java 程序中可以生成完整的 XML 信息,那么用户可以采用类似于上一章中第一节的方式,将 XML 字段映射为 Java 中的字符串,在 save 一个对象的时候,Hibernate 可以处理 XML 字段的情形。

如果 Java 程序中只有 Java 类的各个属性,还需要通过拼装组成 XML,除了通过在 Java 程序中拼装成 XML 字符串,用户还可以调用存储过程来完成该项工作。

当 Java 程序调用 session.save(object) 的时候,Hibernate 读取映射文件中的

<sql-insert>,而后调用它来将该 object 插入到数据库中。需要注意的是存储过程的参数顺序,id 必须放在最后一位,属性顺序和映射文件中的顺序一致。具体例子请查看 eg.hibernate.eg7.Test7 。存储过程的编写请参考 prereq.sql 。

删除

如果主键是映射到关系型列上,那么直接调用 session.delete(object) 就可以。否则就需要在映射文件中编写 <sql-delete>,告诉 Hibernate 调用该语句来删除记录。具体例子请查看 eg.hibernate.eg7.Test7 。

更新

Hibernate 调用 session.update(object) 来更新数据。当 Hibernate 发现 object 数据发生变化时,才会将值更新到数据库中。类似于插入和删除,映射文件中可以使用 <sql-update> 来自定义更新语句。具体例子请参考 eg.hibernate.eg6.Test6 。

当然,如果 XML 文档不大,或者不需要做节点更新,那么可以在 Java 中构造完整的 XML 文档,然后以 String 的方式映射,做整体更新。

XML 文档绑定动态对象

关于 JIBX 和 BeanUtil 的使用,可以参考笔者的另一篇文章“ DB2 pureXML 动态编程组合拳:iBatis+BeanUtils+JiBX ”。事实上,当 Hibernate 从数据库以字符串方式获取到 XML 文档,Java 程序员就可以灵活发挥。笔者成功尝试过将 XML 文档转换成为 BeanUtils 的动态类 : org.apache.commons.beanutils.DynaBean 的实例,而且转换过程可以通过 JIBX 的 Marshaller 来控制,非常灵活。限于篇幅,这里不再展开讨论。

总结

本文通过示例代码介绍了使用 Hibernate 进行基于 DB2 pureXML 的面向对象开发方法,实现了使用 Hibernate 操作 DB2 pureXML 数据库的大多数方面,也仅作为抛砖引玉之用。

声明

本篇文章仅代表笔者个人观点,并不代表 IBM 公司的观点。 IBM 公司不对本文的实现方式作任何保证。

下载

名字 大小 下载方法
DB2XMLHibernate
22K

注意:

  1. 附件包含 Eclipse 项目,prereq.sql 包含了示例代码需要的数据插入存储过程和字典表,请在 sample 数据库中通过 db2 –td% -vf prereq.sql 运行。eg.hibernate.base.MainTester 是各个示例代码的集合调用。读者可以运行该程序测试所有示例,也可以运行各个示例包中的测试代码。

参考资料

学习 获得产品和技术
  • 使用可直接从 developerWorks 下载的 IBM 产品评估试用软件 构建您的下一个开发项目。
  • 现在可以免费使用 DB2。下载 DB2 Express-C,这是为社区提供的 DB2 Express Edition 的免费版本,它提供了与 DB2 Express Edition 相同的核心数据特性,为构建和部署应用程序奠定了坚实的基础。
讨论

火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织