Hibernate 是一个纯 Java 的对象关系映射和持久性框架,它允许您用 XML 配置文件把普通 Java 对象映射到关系数据库表。使用 Hibernate 能够节约大量项目开发时间,因为整个 JDBC 层都由这个框架管理。这意味着您的应用程序的数据访问层位于 Hibernate 之上,完全是从底层数据模型中抽象出来的。
比起其他类似的对象关系映射技术(JDO、实体 bean、内部开发等),Hibernate 有许多优势:它是免费的、开源的,已经成熟到良好的程度,并得到广泛应用,而且还有一个非常活跃的社区论坛。
要把 Hibernate 集成到现有的 Java 项目,则需要执行以下步骤:
您会注意到,不必修改任何 Java 对象,您就可以支持框架。例如,假设您对 Java 应用程序使用的数据库表做了些修改 —— 例如修改了列名。在修改完表之后,您要做的只是更新对应的 XML 配置文件。 您不需要重新编译任何 Java 代码。
Hibernate 提供了一个查询语言,叫作 Hibernate 查询语言(HQL),它与 SQL 很相似。如果您喜欢用老式的 SQL 查询,那么 Hibernate 也为您提供了使用它们的机会。但是我们使用的示例只用 HQL。
HQL 用起来相当简单。您会发现所有的关键字都与您熟悉的 SQL 中的关键字类似,例如 SELECT
、 FROM
和 WHERE
。HQL 与 SQL 的差异在于,您不用针对数据模型(即针对表和列等)直接编写查询,而是应该针对 Java 对象,使用 Java 对象的属性和关系编写查询。
清单 1 演示了一个基本的示例。这个 HQL 代码检索 firstName
为 “John.” 的所有 Individual
。
SELECT * FROM eg.hibernate.mapping.dataobject.Individual WHERE firstName = "John" |
如果想了解更多有关 HQL 语法的内容,那么您可以参阅 Hibernate 的 Web 站点上有关 HQL 的参考材料(请参阅 参考资料,以获得链接)。
功能的核心在于 XML 配置文件。这些文件必须存在于应用程序的 CLASSPATH 中。我们把它们放在示例代码包的 config 目录中(您可以从 参考资料下载)。
我们要研究的第一个文件是 hibernate.cfg.xml。它包含与数据源有关的信息(数据库 URL、模式名称、用户名、口令等),以及对包含映射信息的其他配置文件的引用。
其余的 XML 文件允许您把 Java 类映射到数据库表。稍后我再深入介绍这些文件,但重要的是要清楚它们的文件名要遵守 ClassName.hbm.xml 这个模式。
|
在本文中,我们要研究一个基本示例,演示 Hibernate 如何工作,如何良好地运用三个不同策略,利用 Hibernate 进行对象关系映射。我们的示例是一家保险公司使用的应用程序,公司必须保持客户投保的所有产权的法律记录。我们随本文提供了完整的源代码(请参阅 参考资料);这个代码提供了基本功能,您可以根据它构建全功能的应用程序,例如 Web 或 Swing 应用程序。
我们的示例采用了这类应用程序的经典用例。用户提供搜索参数,查找各种类型的客户(个人、公司、政府机构等),然后显示与指定参数匹配的所有客户列表 —— 即使这些客户的类型不同。用户可以访问同一列表中的某一特定客户更加详细的视图。
在我们的应用程序中,产权由 Right
类表示。 Right
可以是 Lease
也可以是 Property
。 Right
由客户所有。为了表示我们的客户,我们要使用通用类 Person
。 Person
即可以是 Individual
也可以是 Corporation
。当然,保险公司必须知道这些 Right
被分配给哪个 Estate
。您应当同意, Estate
这个术语代表的意义非常泛。所以,我们要用 Land
和 Building
类给我们的开发人员提供更具体的操作对象。
从这个抽象出发,我们可以开发图 1 所示的类模型:
我们的数据库模型是为了介绍将在本文中讨论的三个不同策略而设计的。对于 Right
层次结构来说,我们要使用一个表( TB_RIGHT
),并用 DISCRIMINATOR
列映射到正确的类。对于 Person
结构,我们要使用一个称为 超表( TB_PERSON
)的表,它与另外两个表( TB_CORPORATION
和 TB_INDIVIDUAL
)共享相同的 ID
。第三个层次结构( Estate
)使用两个不同的表( TB_BUILDING
和 TB_LAND
),这两个表通过由两个列( REF_ESTATE_ID
和 REF_ESTATE_TYPE
)组合定义的外键连接在一起。
图 2 显示了这个数据模型:
Hibernate 支持各种各样的 RDBMS,其中任何一种都可以使用我们的示例。但是,本文的示例代码和文本已经针对 HSQLDB(请参阅 参考资料查找链接)进行了调整,这是一个完全用 Java 语言编写的全功能的关系数据库系统。在示例代码包的 sql 目录中,可以找到叫作 datamodel.sql 的文件。这个 SQL 脚本可以创建我们示例中使用的数据模型。
虽然您总能用命令行构建并执行示例代码,但是您可能想在 IDE 中设置项目,以便更好地进行集成。在示例代码包里,您可以找到以下目录:
请确保把必需的 Java 库和 XML 配置文件复制到应用程序的 CLASSPATH。只需要 Hibernate 和 HSQLDB 库,这些代码就可以正确地编译和运行。您可以从 参考资料一节下载这些包。
|
在我们第一个策略中,我们要看看如何映射我们的 Person
层次结构。您会注意到,数据模型与我们的类模型非常接近。所以,我们要为层次结构中的每个类采用一个不同的表,但是所有这些表都必须共享相同的主键(我们很快就会详细说明)。Hibernate 在向数据库中插入新记录时,就会使用这个主键。在访问数据库时,它还会利用同一主键执行 JOIN
操作。
现在我们需要把对象层次结构映射到表模型。我们有三个表( TB_PERSON
、 TB_INDIVIDUAL
、和 TB_CORPORATION
)。前面我们提过,它们都有一个叫作 ID
的列,并将该列作为主键。表之间不一定非要有这样的共享列名称,但是这是一个很好的实践 —— 这样做的话,生成的 SQL 查询更容易阅读。
在清单 2 所示的 XML 映射文件中,您会注意到,在 Person
的映射定义中,声明了两个具体的 <joined-subclass>
类。XML 元素 <id>
映射到顶级表 TB_PERSON
的主键,同时 <key>
元素(来自每个子类)映射到 TB_INDIVIDUAL
的 TB_CORPORATION
表中匹配的主键。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 2.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd"> <hibernate-mapping> <class name="eg.hibernate.mapping.dataobject.Person" table="TB_PERSON" polymorphism="implicit"> <id name="id" column="ID"> <generator class="assigned"/> </id> <set name="rights" lazy="false"> <key column="REF_PERSON_ID"/> <one-to-many class="eg.hibernate.mapping.dataobject.Right" /> </set> <joined-subclass name="eg.hibernate.mapping.dataobject.Individual" table="TB_INDIVIDUAL"> <key column="id"/> <property name="firstName" column="FIRST_NAME" type="java.lang.String" /> <property name="lastName" column="LAST_NAME" type="java.lang.String" /> </joined-subclass> <joined-subclass name="eg.hibernate.mapping.dataobject.Corporation" table="TB_CORPORATION"> <key column="id"/> <property name="name" column="NAME" type="string" /> <property name="registrationNumber" column="REGISTRATION_NUMBER" type="string" /> </joined-subclass> </class> </hibernate-mapping> |
保存 Individual
的一个新实例,形成我们使用 Hibernate 的 Java 代码非常容易,如清单 3 所示:
public Object create(Object object) {
Session session = null;
try {
session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(object);
session.flush();
tx.commit();
...
}
|
接着,Hibernate 生成两个 SQL INSERT
请求,如清单 4 所示。这两个请求面向的是一个 save()
。
insert into TB_PERSON (ID) values (?) insert into TB_INDIVIDUAL (FIRST_NAME, LAST_NAME, id) values (?, ?, ?) |
要想访问数据库中的 Individual
,只需在 HQL 查询中指定类名即可,如清单 5 所示。
public Person findIndividual(Integer id) {
...
session.find(
"select p from " + Individual.class.getName() + " as p where p.id = ?",
new Object[] { id },
new Type[] { Hibernate.INTEGER });
...
}
|
Hibernate 会自动执行 SQL 的 JOIN
,从两个表中检索所有必要信息,如清单 6 所示:
select individual0_.id as ID, individual0_.FIRST_NAME as FIRST_NAME55_,
individual0_.LAST_NAME as LAST_NAME55_
from TB_INDIVIDUAL individual0_
inner join TB_PERSON individual0__1_ on individual0_.id=individual0__1_.ID
where (individual0_.id=? )
|
|
但是,当没有指定具体类时,Hibernate 需要执行 SQL 的 JOIN
,因为它不知道要查询哪个表。在 HQL 查询返回的检索到的所有表的列中,还会返回一个额外的 dynamic 列。Hibernate 使用 clazz
列来初始化和填充返回的对象。我们把这个类叫作决定因子(determination dynamic),与我们在第二个策略中使用的方法相对。
清单 7 显示了如何指定抽象类的 id
属性查询抽象类 Person
,清单 8 显示了 Hibernate 自动生成的 SQL 查询,其中包括表连接:
public Person find(Integer id) {
...
session.find(
"select p from " + Person.class.getName() + " as p where p.id = ?",
new Object[] { id },
new Type[] { Hibernate.INTEGER });
...
}
|
select person0_.ID as ID0_,
casewhen(person0__1_.id is not null, 1,
casewhen(person0__2_.id is not null, 2,
casewhen(person0_.ID is not null, 0, -1))) as clazz_0_,
person0__1_.FIRST_NAME as FIRST_NAME61_0_,
person0__1_.LAST_NAME as LAST_NAME61_0_,
person0__2_.NAME as NAME62_0_,
person0__2_.REGISTRATION_NUMBER as REGISTRA3_62_0_
from TB_PERSON person0_
left outer join TB_INDIVIDUAL person0__1_ on person0_.ID=person0__1_.id
left outer join TB_CORPORATION person0__2_ on person0_.ID=person0__2_.id
where person0_.ID=?
|
|
对于我们的 Right
层次结构,我们只使用一个表( TB_RIGHT
)来保存整体类层次结构。您会注意到, TB_RIGHT
表拥有保存 Right
类层次结构的每个属性所需要的所有列。保存的实例值也就会保存在表中,每个没有使用的列则用 NULL 值填充。(因为它到处都是“洞”,所以我们经常把它叫作 瑞士奶酪表)
在图 3 中,您会注意到, TB_RIGHT
表中包含一个额外的列 DISCRIMINATOR
。Hibernate 用这个列自动初始化对应的类并相应进行填充。这个类用映射文件中的 XML 元素 <discriminator>
进行映射。
|
正如清单 2 所示的 Person
映射文件,在清单 9 中,我们映射了抽象类( Right
)及其所有属性。要映射两个具体类( Lease
和 Property
),就要使用 <subclass>
XML 标签。这个标签非常简单;它要求 name
属性,仅仅是因为 class
标签要求 discriminator-value
属性。Hibernate 将用后一个属性标识它要处理的类。
从 图 1 的类图中您会注意到, discriminator
不是任何 Java 类都有的属性。实际上,它甚至没有映射。它仅仅是 Hibernate 和数据库之间共享的一个技术性的列。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 2.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd"> <hibernate-mapping> <class name="eg.hibernate.mapping.dataobject.Right" table="TB_RIGHT" polymorphism="implicit"> <id name="id" column="ID"> <generator class="assigned"/> </id> <discriminator> <column name="DISCRIMINATOR"/> </discriminator> <property name="date" column="DATE" type="java.sql.Date" /> <many-to-one name="person" class="eg.hibernate.mapping.dataobject.Person" column="REF_PERSON_ID"/> <any name="estate" meta-type="string" id-type="java.lang.Integer"> <meta-value value="LND" class="eg.hibernate.mapping.dataobject.Land"/> <meta-value value="BLD" class="eg.hibernate.mapping.dataobject.Building"/> <column name="REF_ESTATE_TYPE"/> <column name="REF_ESTATE_ID"/> </any> <subclass name="eg.hibernate.mapping.dataobject.Property" discriminator-value="PRO"/> <subclass name="eg.hibernate.mapping.dataobject.Lease" discriminator-value="LEA"> <property name="duration" column="DURATION" type="java.lang.Integer" /> </subclass> </class> </hibernate-mapping> |
在清单 9 的映射文件中,您会注意到 Right
和 Person
层次结构之间的“多对一”关系,它(实质上)与 Person
层次结构(一对多)的关系正好相反。还请注意 Right
和 Estate
层次结构之间的关系;稍后我们将在本文中介绍这层关系。
使用第一个策略时,Hibernate 在访问数据库时生成了非常有效的 SQL 语句。当我们查询具体类时,如清单 10 所示,会在 discriminator
的 Hibernate 过滤器上自动取值 —— 这是件好事,因为这意味着 Hibernate 只读取与指定类对应的列。
select property0_.ID as ID, property0_.DATE as DATE, property0_.REF_PERSON_ID as REF_PERS4_, property0_.REF_ESTATE_TYPE as REF_ESTA5_, property0_.REF_ESTATE_ID as REF_ESTA6_ from TB_RIGHT property0_ where property0_.DISCRIMINATOR='PRO' |
当我们查询抽象类时,事情变得有些复杂。因为 Hibernate 不知道您要查询哪个特定的类,所以必须读取每个列(包括 discriminator 类),然后才能决定要初始化哪个类,最后再填充它。接下来,discriminator 充当的角色与第一个策略中 clazz
列充当的角色相同。但是这个方法显然更死板,因为类名直接派生自 discriminator 的值。
select right0_.ID as ID, right0_.DISCRIMINATOR as DISCRIMI2_, right0_.DATE as DATE, right0_.REF_PERSON_ID as REF_PERS4_, right0_.REF_ESTATE_TYPE as REF_ESTA5_, right0_.REF_ESTATE_ID as REF_ESTA6_, right0_.DURATION as DURATION from TB_RIGHT right0_ |
|
关于第二个策略,有一个需要重点考虑的地方:为了让它工作,必须把所有非共享列设置为 NULLABLE
。因为开发人员通常会依赖数据库的约束,所以生成的表可能非常难以处理。(毕竟,把有持续时间的 Lease
设置为 NULL
没多大意义!)
解决方案之一是用 数据库级检测约束。您可以根据 DISCRIMINATOR
的值,定义一套要实施的规则,如清单 12 所示。当然,数据库引擎必须支持这些特性。而且,由于必须同时为全部具体类用一个有效表达式表示这些约束,所以当层次结构发展的时候,维护会很困难。
alter table TB_RIGHT add constraint CHK_RIGHT check( (discriminant ='DPP' and date is null and duration is null) or (discriminant ='DLM' and date is not null and duration is not null)); |
|
我们的第三个,也是最后一个策略可能是三个策略当中最有想象力的:每个具体类一个表,抽象超类 Estate
没有表。我们依靠 Hibernate 提供对 多态(polymorphism)的支持。在清单 3 所示的 XML 映射文件中,您会注意到,其中只映射了两个具体类( Building
和 Land
):
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 2.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd"> <hibernate-mapping> <class name="eg.hibernate.mapping.dataobject.Land" table="TB_LAND" polymorphism="implicit"> <id name="id" column="ID"> <generator class="assigned"/> </id> <property name="description" column="DESCRIPTION" type="java.lang.String" /> <property name="squareFeet" column="SQUARE_FEET" type="java.lang.Double"/> </class> <class name="eg.hibernate.mapping.dataobject.Building" table="TB_BUILDING" polymorphism="implicit"> <id name="id" column="ID"> <generator class="assigned"/> </id> <property name="description" column="DESCRIPTION" type="java.lang.String" /> <property name="address" column="ADDRESS" type="java.lang.String"/> </class> </hibernate-mapping> |
|
当您查看清单 13 中的映射文件时,您的第一个反应可能是说:“呵!这个映射与我每天使用的没什么不同啊!这里没什么重要的东西!”而且您这么想应当是对的。实际上,第三个策略只需要一个条件:需要把 polymorphism
属性设置为 implicit
。
即使在映射文件中找不到 Estate
类,它仍然存在于类层次结构之中。而且,因为两个映射的类( Building
和 Land
)是从 Estate
中继承而来,所以我们可以在 HQL 查询中使用这个抽象超类,如清单 14 所示。Hibernate 会用 内省(introspection) 找到扩展这个抽象类的类,以便依次为每个子类执行对应的 SQL 查询。
public Estate find(Integer id) { ... List objects = session.find( "select e from " + Estate.class.getName() + " as e where e.id = ?", new Object[] { id }, new Type[] { Hibernate.INTEGER }); ... } |
为了找到与指定 ID
匹配的 Estate
,Hibernate 必须把清单 15 中的两个查询提交给数据库。
select land0_.ID as ID, land0_.DESCRIPTION as DESCRIPT2_, land0_.SQUARE_FEET as SQUARE_F3_ from TB_LAND land0_ where (land0_.ID=? ) select building0_.ID as ID, building0_.DESCRIPTION as DESCRIPT2_, building0_.ADDRESS as ADDRESS from TB_BUILDING building0_ where (building0_.ID=? ) |
正如我们在第二个策略中看到的,在 Right
和 Estate
类之间存在着 多对一 关系。在一般的表述中,这两个表的关系可以这么表述:“一个 Estate
可以指向 许多 Right
。但是每个 Right
只能指向 一个 Estate
。”但是从我们数据模型的角度来看,我们没有一个惟一的表可以用来创建我们的外键约束,就像 TB_RIGHT
和 TB_PERSON
之间那样。这使得我们几乎不可能创建外键。幸运的是,Hibernate 为我们提供了一个非常强大的 XML 映射元素 —— <any>
标签,它的用法如清单 16 所示。
<any name="estate" meta-type="string" id-type="java.lang.Integer"> <meta-value value="LND" class="eg.hibernate.mapping.dataobject.Land"/> <meta-value value="BLD" class="eg.hibernate.mapping.dataobject.Building"/> <column name="REF_ESTATE_TYPE"/> <column name="REF_ESTATE_ID"/> </any> |
|
我们进一步查看我们的新映射。我们的 虚拟外键 基于 TB_RIGHT
表的两个列。第一个列( REF_ESTATE_TYPE
)包含 discriminator 字符串,用这个字符串映射对应的类名。第二个( REF_ESTATE_ID
)是另外一个表的主键的列名。使用默认设置时,Hibernate 会在第一个列中保存映射的类名,这么做可能会非常消耗空间、没有效率(特别是在代码重构修改类名的时候)。谢天谢地,Hibernate 还提供了一个用 <meta-value>
XML 元素把类名映射到字符串约束的方法。这些约束的作用与在第二个策略中讨论的 discriminator 的作用相同。再次声明,这些特性只包含 Hibernate 和数据库,所以不会改变类的层次结构。
虽然标准 SQL 不允许对多个表同时针对指定列进行参考约束,但是仍有可能添加触发器,根据读取的 discriminator 值,触发器会检测目录中是否有数据。但是,这样的 完整性实施 方法可能非常难以维护,也有可能降低数据的整体性能。
|
在使用 Hibernate 内置的多态时需要记住一件事:如果您一不小心,把所有的类都用 polymorphism
属性设置为 implicit
,那么您检索到的信息可能要比您想要的多得多。清单 17 显示了一种使用两个词的 HQL 查询来检索 整个数据库 的方法。
public List all() {
...
List objects = session.find("
from Object");
...
}
|
该查询的功能非常强大,您觉得呢?当然,我们之中没有多少人需要只用一个 HQL 查询检索整个数据库。这个(没有实际意义)的示例的目的就是为了显示隐式多态的能力。您可以利用这个能力避免把无用的、耗费资源的 SQL 查询发送到数据库。
|
在本文中,我们试图向您提供一个相当简单的实现示例,演示 Hibernate 提供的三个映射策略。回头来看,每个策略都有自己的优势与不足:
不管您选择哪种策略,都要记住,在整个过程当中,无需修改 Java 类,这意味着业务对象与持续性框架之间一点联系都没有。正是这样高水平的灵活性使 Hibernate 在对象关系 Java 项目中如此流行。