映射继承关系

第14章 映射继承关系
在域模型中,类与类之间除了关联关系和聚集关系,还可以存在继承关系,在图14-1所示的域模型中,Company类和Employee类之间为一对多的双向关联关系(假定不允许雇员同时在多个公司兼职),Employee类为抽象类,因此它不能被实例化,它有两个具体的子类:HourlyEmployee类和SalariedEmployee类。由于Java只允许一个类最多有一个直接的父类,因此Employee类、HourlyEmployee类和SalariedEmployee类构成了一棵继承关系树。

图14-1 包含继承关系的域模型
在面向对象的范畴中,还存在多态的概念,多态建立在继承关系的基础上。简单地理解,多态是指当一个Java应用变量被声明为Employee类时,这个变量实际上既可以引用HourlyEmployee类的实例,也可以引用SalariedEmployee类的实例。以下这段程序代码就体现了多态:
List employees= businessService.findAllEmployees();
Iterator it=employees.iterator();
while(it.hasNext()){
Employee e=(Employee)it.next();
if(e instanceof HourlyEmployee){
System.out.println(e.getName()+" "+((HourlyEmployee)e).getRate());
}else
System.out.println(e.getName()+" "+((SalariedEmployee)e).getSalary());
}
BusinessService类的findAllEmployees()方法通过Hibernate API从数据库中检索出所有Employee对象。findAllEmployees()方法返回的集合既包含HourlyEmployee类的实例,也包含SalariedEmployee类的实例,这种查询被称为多态查询。以上程序中变量e被声明为Employee类型,它实际上既可能引用HourlyEmployee类的实例,也可能引用SalariedEmployee类的实例。
此外,从Company类到Employee类为多态关联,因为Company类的employees集合中可以包含HourlyEmployee类和SalariedEmployee类的实例。从Employee类到Company类不是多态关联,因为Employee类的company属性只会引用Company类本身的实例。
数据库表之间并不存在继承关系,那么如何把域模型的继承关系映射到关系数据模型中呢?本章将介绍以下三种映射方式:
l 继承关系树的每个具体类对应一个表:关系数据模型完全不支持域模型中的继承关系和多态。
l 继承关系树的根类对应一个表:对关系数据模型进行非常规设计,在数据库表中加入额外的区分子类型的字段。通过这种方式,可以使关系数据模型支持继承关系和多态。
l 继承关系树的每个类对应一个表:在关系数据模型中用外键参照关系来表示继承关系。
具体类是指非抽象的类,具体类可以被实例化。HourlyEmployee类和SalariedEmployee类就是具体类。
以上每种映射方式都有利有弊,本章除了介绍每种映射方式的具体步骤,还介绍了它们的适用范围。
14.1 继承关系树的每个具体类对应一个表
把每个具体类映射到一张表是最简单的映射方式。如图14-2所示,在关系数据模型中只需定义COMPANIES、HOURLY_EMPLOYEES和SALARIED_EMPLOYEES表。为了叙述的方便,下文把HOURLY_EMPLOYEES表简称为HE表,把SALARIED_EMPLOYEES表简称为SE表。HourlyEmployee类和HE表对应,HourlyEmployee类本身的rate属性,以及从Employee类中继承的id属性和name属性,在HE表中都有对应的字段。此外,HourlyEmployee类继承了Employee类与Company类的关联关系,与此对应,在HE表中定义了参照COMPANIES表的COMPANY_ID外键。
SalariedEmployee类和SE表对应,SalariedEmployee类本身的salary属性,以及从Employee类中继承的id属性和name属性,在SE表中都有对应的字段。此外,SalariedEmployee类继承了Employee类与Company类的关联关系,与此对应,在SE表中定义了参照COMPANIES表的COMPANY_ID外键。
Company类、HourlyEmployee类和SalariedEmployee类都有相应的映射文件,而Employee类没有相应的映射文件。图14-3显示了持久化类、映射文件和数据库表之间的对应关系。











图14-2 每个具体类对应一个表







图14-3 持久化类、映射文件和数据库表之间的对应关系
如果Employee类不是抽象类,即Employee类本身也能被实例化,那么还需要为Employee类创建对应的EMPLOYEES表,此时HE表和SE表的结构仍然和图14-2中所示的一样。这意味着在EMPLOYEES表、HE表和SE表中都定义了相同的NAME字段以及参照COMPANIES表的外键COMPANY_ID。另外,还需为Employee类创建单独的Employee.hbm.xml文件。
14.1.1 创建映射文件
从Company类到Employee类是多态关联,但是由于关系数据模型没有描述Employee类和它的两个子类的继承关系,因此无法映射Company类的employees集合。例程14-1是Company.hbm.xml文件的代码,该文件仅仅映射了Company类的id和name属性。
例程14-1 Company.hbm.xml
<hibernate-mapping >
<class name="mypack.Company" table="COMPANIES" >
<id name="id" type="long" column="ID">
<generator class="increment"/>
</id>

<property name="name" type="string" column="NAME" />

</class>
</hibernate-mapping>
HourlyEmployee.hbm.xml文件用于把HourlyEmployee类映射到HE表,在这个映射文件中,除了需要映射HourlyEmployee类本身的rate属性,还需要映射从Employee类中继承的name属性,此外还要映射从Employee类中继承的与Company类的关联关系。例程14-2是HourlyEmployee.hbm.xml文件的代码。
例程14-2 HourlyEmployee.hbm.xml
<hibernate-mapping >
<class name="mypack.HourlyEmployee" table="HOURLY_EMPLOYEES">
<id name="id" type="long" column="ID">
<generator class="increment"/>
</id>

<property name="name" type="string" column="NAME" />
<property name="rate" column="RATE" type="double" />

<many-to-one
name="company"
column="COMPANY_ID"
class="mypack.Company"
/>
</class>
</hibernate-mapping>
SalariedEmployee.hbm.xml文件用于把SalariedEmployee类映射到SE表,在这个映射文件中,除了需要映射SalariedEmployee类本身的salary属性,还需要映射从Employee类中继承的name属性,此外还要映射从Employee类中继承的与Company类的关联关系。例程14-3是SalariedEmployee.hbm.xml文件的代码。
例程14-3 SalariedEmployee.hbm.xml
<hibernate-mapping >
<class name="mypack.SalariedEmployee" table="SALARIED_EMPLOYEES">
<id name="id" type="long" column="ID">
<generator class="increment"/>
</id>

<property name="name" type="string" column="NAME" />
<property name="salary" column="SALARY" type="double" />

<many-to-one
name="company"
column="COMPANY_ID"
class="mypack.Company"
/>
</class>
</hibernate-mapping>
由于Employee类没有相应的映射文件,因此在初始化Hibernate时,只需向Configuration对象中加入Company类、HourlyEmployee类和SalariedEmployee类:
Configuration config = new Configuration();
config.addClass(Company.class)
.addClass(HourlyEmployee.class)
.addClass(SalariedEmployee.class);
14.1.2 操纵持久化对象
这种映射方式不支持多态查询,在本书第11章的11.1.6节(多态查询)介绍了多态查询的概念。对于以下查询语句:
List employees=session.find("from Employee");
如果Employee类是抽象类,那么Hibernate会抛出异常。如果Employee类是具体类,那么Hibernate仅仅查询EMPLOYEES表,检索出Employee类本身的实例,但不会检索出它的两个子类的实例。本节的范例程序位于配套光盘的sourcecode/chapter14/14.1目录下,运行该程序前,需要在SAMPLEDB数据库中手工创建COMPANIES表、HE表和SE表,然后加入测试数据,相关的SQL脚本文件为/14.1/schema/sampledb.sql。
在chapter14目录下有四个ANT的工程文件,分别为build1.xml、build2.xml、build3.xml和build4.xml,它们的区别在于文件开头设置的路径不一样,例如在build1.xml文件中设置了以下路径:
<property name="source.root" value="14.1/src"/>
<property name="class.root" value="14.1/classes"/>
<property name="lib.dir" value="lib"/>
<property name="schema.dir" value="14.1/schema"/>
在DOS命令行下进入chapter14根目录,然后输入命令:
ant -file build1.xml run
就会运行BusinessService类。ANT命令的-file选项用于显式指定工程文件。BusinessService类用于演示操纵Employee类的对象的方法,例程14-4是它的源程序。
例程14-4 BusinessService.java
public class BusinessService{
public static SessionFactory sessionFactory;
static{
try{
Configuration config = new Configuration();
config.addClass(Company.class)
.addClass(HourlyEmployee.class)
.addClass(SalariedEmployee.class);
sessionFactory = config.buildSessionFactory();
}catch(Exception e){e.printStackTrace();}
}

public void saveEmployee(Employee employee) throws Exception{……}
public List findAllEmployees() throws Exception{……}
public Company loadCompany(long id) throws Exception{……}

public void test() throws Exception{
List employees=findAllEmployees();
printAllEmployees(employees.iterator());

Company company=loadCompany(1);
printAllEmployees(company.getEmployees().iterator());

Employee employee=new HourlyEmployee("Mary",300,company);
saveEmployee(employee);

}

private void printAllEmployees(Iterator it){
while(it.hasNext()){
Employee e=(Employee)it.next();
if(e instanceof HourlyEmployee){
System.out.println(((HourlyEmployee)e).getRate());
}else
System.out.println(((SalariedEmployee)e).getSalary());
}
}
public static void main(String args[]) throws Exception {
new BusinessService().test();
sessionFactory.close();
}
}
BusinessService的main()方法调用test()方法,test()方法依次调用以下方法。
l findAllEmployees():检索数据库中所有的Employee对象。
l loadCompany():加载一个Company对象。
l saveEmployee():保存一个Employee对象。
(1)运行findAllEmployees()方法,它的代码如下:
List results=new ArrayList();
tx = session.beginTransaction();
List hourlyEmployees=session.find("from HourlyEmployee");
results.addAll(hourlyEmployees);

List salariedEmployees=session.find("from SalariedEmployee");
results.addAll(salariedEmployees);

tx.commit();
return results;
为了检索所有的Employee对象,必须分别检索所有的HourlyEmployee实例和SalariedEmployee实例,然后把它们合并到同一个集合中。在运行Session的第一个find()方法时,Hibernate执行以下select语句:
select * from HOURLY_EMPLOYEES;
select * from COMPANIES where ID=1;
从HourlyEmployee类到Company类不是多态关联,在加载HourlyEmployee对象时,会同时加载与它关联的Company对象。
在运行Session的第二个find()方法时,Hibernate执行以下select语句:
select * from SALARIED_EMPLOYEES;
从SalariedEmployee类到Company类不是多态关联,在加载SalariedEmployee对象时,会同时加载与它关联的Company对象。在本书提供的测试数据中,所有HourlyEmployee实例和SalariedEmployee实例都与OID为1的Company对象关联,由于该Company对象已经被加载到内存中,所以Hibernate不再需要执行检索该对象的select语句。
(2)运行loadCompany()方法,它的代码如下:
tx = session.beginTransaction();
Company company=(Company)session.load(Company.class,new Long(id));

List hourlyEmployees=session.find("from HourlyEmployee h where h.company.id="+id);
company.getEmployees().addAll(hourlyEmployees);

List salariedEmployees=session.find("from SalariedEmployee s where s.company.id="+id);
company.getEmployees().addAll(salariedEmployees);

tx.commit();
return company;
由于这种映射方式不支持多态关联,因此由Session的load()方法加载的Company对象的employees集合中不包含任何Employee对象。BusinessService类必须负责从数据库中检索出所有与Company对象关联的HourlyEmployee对象以及SalariedEmployee对象,然后把它们加入到employees集合中。
(3)运行saveEmployee(Employee employee)方法,它的代码如下:
tx = session.beginTransaction();
session.save(employee);
tx.commit();
在test()方法中,创建了一个HourlyEmployee实例,然后调用saveEmployee()方法保存这个实例:
Employee employee=new HourlyEmployee("Mary",300,company);
saveEmployee(employee);
Session的save()方法能判断employee变量实际引用的实例的类型,如果employee变量引用HourlyEmployee实例,就向HE表插入一条记录,执行如下insert语句:
insert into HOURLY_EMPLOYEES(ID,NAME,RATE,CUSTOMER_ID)
values(3, 'Mary',300,1);
如果employee变量引用SalariedEmployee实例,就向SE表插入一条记录。
14.2 继承关系树的根类对应一个表
这种映射方式只需为继承关系树的Employee根类创建一张表EMPLOYEES。如图14-4所示,在EMPLOYEES表中不仅提供和Employee类的属性对应的字段,还要提供和它的两个子类的所有属型对应的字段,此外,EMPLOYEES表中需要额外加入一个字符串类型的EMPLOYEE_TYPE字段,用于区分Employee的具体类型。






图14-4 继承关系树的根类对应一个表
Company类和Employee类有相应的映射文件,而HourlyEmployee类和SalariedEmployee类没有相应的映射文件。图14-5显示了持久化类、映射文件和数据库表之间的对应关系。







图14-5 持久化类、映射文件和数据库表之间的对应关系

14.2.1 创建映射文件
从Company类到Employee类是多态关联,由于关系数据模型描述了Employee类和它的两个子类的继承关系,因此可以映射Company类的employees集合。例程14-5是Company.hbm.xml文件的代码,该文件不仅映射了Company类的id和name属性,还映射了它的employees集合。
例程14-5 Company.hbm.xml
<hibernate-mapping >

<class name="mypack.Company" table="COMPANIES" >
<id name="id" type="long" column="ID">
<generator class="increment"/>
</id>

<property name="name" type="string" column="NAME" />
<set
name="employees"
inverse="true"
lazy="true" >
<key column="COMPANY_ID" />
<one-to-many class="mypack.Employee" />
</set>

</class>
</hibernate-mapping>
Employee.hbm.xml文件用于把Employee类映射到EMPLOYEES表,在这个映射文件中,除了需要映射Employee类本身的属性,还需要在<subclass>元素中映射两个子类的属性。例程14-6是Employee.hbm.xml文件的代码。
例程14-6 Employee.hbm.xml
<hibernate-mapping >
<class name="mypack.Employee" table="EMPLOYEES">
<id name="id" type="long" column="ID">
<generator class="increment"/>
</id>
<discriminator column="EMPLOYEE_TYPE" type="string" />
<property name="name" type="string" column="NAME" />

<many-to-one
name="company"
column="COMPANY_ID"
class="mypack.Company"
/>

<subclass name="mypack.HourlyEmployee" discriminator-value="HE" >
<property name="rate" column="RATE" type="double" />
</subclass>

<subclass name="mypack.SalariedEmployee" discriminator-value="SE" >
<property name="salary" column="SALARY" type="double" />
</subclass>

</class>

</hibernate-mapping>
在Employee.hbm.xml文件中,<discriminator>元素指定EMPLOYEES表中用于区分Employee类型的字段为EMPLOYEE_TYPE,两个<subclass>元素用于映射HourlyEmployee类和SalariedEmployee类,<subclass>元素的discriminator-value属性指定EMPLOYEE_TYPE字段的取值。EMPLOYEES表中有以下记录:

ID NAME EMPLOYEE_TYPE RATE SALARY COMPANY_ID

1 Tom HE 100 NULL 1
2 Mike HE 200 NULL 1
3 Jack SE NULL 5000 1
4 Linda SE NULL 6000 1

其中ID为1和2的记录的EMPLOYEE_TYPE字段的取值为“HE”,因此它们对应HourlyEmployee类的实例,其中ID为3和4的记录的EMPLOYEE_TYPE字段的取值为“SE”,因此它们对应SalariedEmployee类的实例。
这种映射方式要求EMPLOYEES表中和子类属性对应的字段允许为null,例如ID为1和2的记录的SALARY字段为null,而ID为3和4的记录的RATE字段为null。如果业务需求规定SalariedEmployee对象的rate属性不允许为null,显然无法在EMPLOYEES表中为SALARY字段定义not null约束,可见这种映射方式无法保证关系数据模型的数据完整性。
由于HourlyEmployee类和SalariedEmployee类没有单独的映射文件,因此在初始化Hibernate时,只需向Configuration对象中加入Company类和Employee类:
Configuration config = new Configuration();
config.addClass(Company.class)
.addClass(Employee.class);
如果Employee类不是抽象类,即它本身也能被实例化,那么可以在<class>元素中定义它的discriminator值,形式如下:
<class name="mypack.Employee" table="EMPLOYEES" discriminator-value="EE">
以上代码表明,如果EMPLOYEES表中一条记录的EMPLOYEE_TYPE字段的取值为“EE”,那么它对应Employee类本身的实例。
14.2.2 操纵持久化对象
这种映射方式支持多态查询,对于以下查询语句:
List employees=session.find("from Employee");
Hibernate会检索出所有的HourlyEmployee对象和SalariedEmployee对象。此外,也可以单独查询Employee类的两个子类的实例,例如:
List hourlyEmployees=session.find("from HourlyEmployee");
本节的范例程序位于配套光盘的sourcecode/chapter14/14.2目录下,运行该程序前,需要在SAMPLEDB数据库中手工创建COMPANIES表和EMPLOYEES表,然后加入测试数据,相关的SQL脚本文件为/14.2/schema/sampledb.sql。
在DOS命令行下进入chapter14根目录,然后输入命令:
ant -file build2.xml run
就会运行BusinessService类。BusinessService的main()方法调用test()方法,test()方法依次调用以下方法:
l findAllHourlyEmployees():检索数据库中所有的HourlyEmployee对象。
l findAllEmployees():检索数据库中所有的Employee对象。
l loadCompany():加载一个Company对象。
l saveEmployee():保存一个Employee对象。
(1)运行findAllHourlyEmployees()方法,它的代码如下:
tx = session.beginTransaction();
List results=session.find("from HourlyEmployee");
tx.commit();
return results;
在运行Session的find()方法时,Hibernate执行以下select语句:
select * from EMPLOYEES where EMPLOYEE_TYPE='HE' ;
select * from COMPANIES where ID=1;
在加载HourlyEmployee对象时,还会同时加载与它关联的Company对象。
(2)运行findAllEmployees()方法,它的代码如下:
tx = session.beginTransaction();
List results=session.find("from Employee");
tx.commit();
return results;
在运行Session的find()方法时,Hibernate执行以下select语句:
select * from EMPLOYEES;
select * from COMPANIES where ID=1;
在这种映射方式下,Hibernate支持多态查询,对于从EMPLOYEES表获得的查询结果,如果EMPLOYEE_TYPE字段取值为“HE”,就创建HoulyEmployee实例,如果EMPLOYEE_TYPE字段取值为“SE”,就创建SalariedEmployee实例,这些实例所关联的Company对象也被加载。
(3)运行loadCompany()方法,它的代码如下:
tx = session.beginTransaction();
Company company=(Company)session.load(Company.class,new Long(id));
Hibernate.initialize(company.getEmployees());
tx.commit();
这种映射方式支持多态关联。如果在Company.hbm.xml文件中对employees集合设置了立即检索策略,那么Session的load()方法加载的Company对象的employees集合中包含所有关联的Employee对象。由于本书提供的Company.hbm.xml文件对employees集合设置了延迟检索策略,因此以上程序代码还通过Hibernate类的静态initialize()方法来显式初始化employees集合。
(4)运行saveEmployee(Employee employee)方法,它的代码如下:
tx = session.beginTransaction();
session.save(employee);
tx.commit();
在test()方法中,创建了一个HourlyEmployee实例,然后调用saveEmployee()方法保存这个实例:
Employee employee=new HourlyEmployee("Mary",300,company);
saveEmployee(employee);
Session的save()方法能判断employee变量实际引用的实例的类型,如果employee变量引用HourlyEmployee实例,就执行如下insert语句:
insert into EMPLOYEES(ID,NAME,RATE,EMPLOYEE_TYPE,CUSTOMER_ID)
values(5, 'Mary ',300, 'HE',1);
以上insert语句没有为SalariedEmployee类的salary属性对应的SALARY字段赋值,因此这条记录的SALARY字段为null。
14.3 继承关系树的每个类对应一个表
在这种映射方式下,继承关系树的每个类以及接口都对应一个表。在本例中,需要创建EMPLOYEES、HE和SE表。如图14-6所示,EMPLOYEES表仅包含和Employee类的属性对应的字段,HE表仅包含和HourlyEmployee类的属性对应的字段,SE表仅包含和SalariedEmployee类的属性对应的字段。此外,HE表和SE表都以EMPLOYEE_ID字段作为主键,该字段还同时作为外键参照EMPLOYEES表。













图14-6 继承关系树的每个类对应一个表
Company类和Employee类有相应的映射文件,而HourlyEmployee类和SalariedEmployee类没有相应的映射文件。图14-7显示了持久化类、映射文件和数据库表之间的对应关系。







图14-7 持久化类、映射文件和数据库表之间的对应关系
14.3.1 创建映射文件
从Company类到Employee类是多态关联,由于关系数据模型描述了Employee类和它的两个子类的继承关系,因此可以映射Company类的employees集合。例程14-7是Company.hbm.xml文件的代码,该文件不仅映射了Company类的id和name属性,还映射了它的employees集合。
例程14-7 Company.hbm.xml
<hibernate-mapping >
<class name="mypack.Company" table="COMPANIES" >
<id name="id" type="long" column="ID">
<generator class="increment"/>
</id>

<property name="name" type="string" column="NAME" />
<set
name="employees"
inverse="true"
lazy="true" >
<key column="COMPANY_ID" />
<one-to-many class="mypack.Employee" />
</set>

</class>
</hibernate-mapping>
Employee.hbm.xml文件用于把Employee类映射到EMPLOYEES表,在这个映射文件中,除了需要映射Employee类本身的属性,还需要在<joined-subclass>元素中映射两个子类的属性。例程14-8是Employee.hbm.xml文件的代码。
例程14-8 Employee.hbm.xml
<hibernate-mapping >

<class name="mypack.Employee" table="EMPLOYEES">
<id name="id" type="long" column="ID">
<generator class="increment"/>
</id>
<property name="name" type="string" column="NAME" />

<many-to-one
name="company"
column="COMPANY_ID"
class="mypack.Company"
/>

<joined-subclass name="mypack.HourlyEmployee" table="HOURLY_EMPLOYEES" >
<key column="EMPLOYEE_ID" />
<property name="rate" column="RATE" type="double" />
</joined-subclass>

<joined-subclass name="mypack.SalariedEmployee" table="SALARIED_EMPLOYEES" >
<key column="EMPLOYEE_ID" />
<property name="salary" column="SALARY" type="double" />
</joined-subclass>

</class>
</hibernate-mapping>
在Employee.hbm.xml文件中,两个<joined-subclass>元素用于映射HourlyEmployee类和SalariedEmployee类,<joined-subclass>元素的<key>子元素指定HE表和SE表中既作为主键又作为外键的EMPLOYEE_ID字段。图14-8显示了EMPLOYEES表、HE表和SE表中记录的参照关系。

图14-8 EMPLOYEES表、HE表和SE表中记录的参照关系
由于HourlyEmployee类和SalariedEmployee类没有单独的映射文件,因此在初始化Hibernate时,只需向Configuration对象中加入Company类和Employee类:
Configuration config = new Configuration();
config.addClass(Company.class)
.addClass(Employee.class);
也可以在单独的映射文件中配置<subclass>或<joined-subclass>元素,但此时必须显式设定它们的extends属性。例如可以在单独的HourlyEmployee.hbm.xml文件中映射HourlyEmployee类:
<hibernate-mapping >
<joined-subclass
name="mypack.HourlyEmployee"
table="HOURLY_EMPLOYEES"
extends="mypack.Employee" >
……
</joined-class>
<hibernate-mapping >
由于HourlyEmployee类的映射代码不位于Employee.hbm.xml文件中,因此在初始化Hibernate时,不仅需要向Configuration对象中加入Company类和Employee类,还需要加入HourlyEmployee类,并且必须先加入Employee父类,再加入HourlyEmployee子类:
Configuration config = new Configuration();
config.addClass(Company.class)
.addClass(Employee.class)
.addClass(HourlyEmployee.class);
如果颠倒加入Employee类和HourlyEmployee子类的顺序,Hibernate在执行addClass()方法时会抛出HibernateMappingException。
14.3.2 操纵持久化对象
这种映射方式支持多态查询,对于以下查询语句:
List employees=session.find("from Employee");
Hibernate会检索出所有的HourlyEmployee对象和SalariedEmployee对象。此外,也可以单独查询Employee类的两个子类的实例,例如:
List hourlyEmployees=session.find("from HourlyEmployee");
本节的范例程序位于配套光盘的sourcecode/chapter14/14.3目录下,运行该程序前,需要在SAMPLEDB数据库中手工创建COMPANIES表、EMPLOYEES表、HE表和SE表,然后加入测试数据,相关的SQL脚本文件为/14.3/schema/sampledb.sql。
在DOS命令行下进入chapter14根目录,然后输入命令:
ant -file build3.xml run
就会运行BusinessService类。BusinessService的main()方法调用test()方法,test()方法依次调用以下方法:
l findAllHourlyEmployees():检索数据库中所有的HourlyEmployee对象。
l findAllEmployees():检索数据库中所有的Employee对象。
l loadCompany():加载一个Company对象。
l saveEmployee():保存一个Employee对象。
(1)运行findAllHourlyEmployees()方法,它的代码如下:
tx = session.beginTransaction();
List results=session.find("from HourlyEmployee");
tx.commit();
return results;
在运行Session的find()方法时,Hibernate执行以下select语句:
select * from HOURLY_EMPLOYEES he inner join EMPLOYEES e
on he.EMPLOYEE_ID=e.ID;
select * from COMPANIES where ID=1;
Hibernate通过HE表与EMPLOYEES表的内连接获得HourlyEmployee对象的所有属性值,此外,在加载HourlyEmployee对象时,还会同时加载与它关联的Company对象。
(2)运行findAllEmployees()方法,它的代码如下:
tx = session.beginTransaction();
List results=session.find("from Employee");
tx.commit();
return results;
在运行Session的find()方法时,Hibernate执行以下select语句:
select * from EMPLOYEES e
left outer join HOURLY_EMPLOYEES he on e.ID=he.EMPLOYEE_ID
left outer join SALARIED_EMPLOYEES se on e.ID=se.EMPLOYEE_ID;


select * from COMPANIES where ID=1;
Hibernate把EMPLOYEES表与HE表以及SE表进行左外连接,从而获得HourlyEmployee对象和SalariedEmployee对象的所有属性值。在这种映射方式下,Hibernate支持多态查询,对于以上查询语句获得的查询结果,如果HE表的EMPLOYEE_ID字段不为null,就创建HoulyEmployee实例,如果SE表的EMPLOYEE_ID字段不为null,就创建SalariedEmployee实例,这些实例所关联的Company对象也被加载。
(3)运行loadCompany()方法,它的代码如下:
tx = session.beginTransaction();
Company company=(Company)session.load(Company.class,new Long(id));
Hibernate.initialize(company.getEmployees());
tx.commit();
这种映射方式支持多态关联。如果在Company.hbm.xml文件中对employees集合设置了立即检索策略,那么Session的load()方法加载的Company对象的employees集合中包含所有关联的Employee对象。由于本书提供的Company.hbm.xml文件对employees集合设置了延迟检索策略,因此以上程序代码还通过Hibernate类的静态initialize()方法来显式初始化employees集合。
(4)运行saveEmployee(Employee employee)方法,它的代码如下:
tx = session.beginTransaction();
session.save(employee);
tx.commit();
在test()方法中,创建了一个HourlyEmployee实例,然后调用saveEmployee()方法保存这个实例:
Employee employee=new HourlyEmployee("Mary",300,company);
saveEmployee(employee);
Session的save()方法能判断employee变量实际引用的实例的类型,如果employee变量引用HourlyEmployee实例,就执行如下insert语句:
insert into EMPLOYEES (ID,NAME, COMPANY_ID) values (5, 'Mary', 1);
insert into HOURLY_EMPLOYEES (EMPLOYEE_ID ,RATE) values (5, 300);
可见,每保存一个HourlyEmployee对象,需要分别向EMPLOYEES表和HE表插入一条记录,EMPLOYEES表的记录和HE表的记录共享同一个主键。
14.4 选择继承关系的映射方式
本章介绍的三种映射方式各有优缺点,表14-1对这三种映射方式做了比较。

表14-1 比较三种映射方式
比较方面 每个具体类对应一个表 根类对应一个表 每个类对应一个表
关系数据模型的复杂度 缺点:每个具体类对应一个表,这些表中包含重复字段 优点:只需创建一个表 缺点:表的数目最多,并且表之间还有外键参照关系
查询性能 缺点:如果要查询父类的对象,必须查询所有具体的子类对应的表 优点:有很好的查询性能,无需进行表的连接 缺点:需要进行表的内连接或左外连接
数据库Schema的可维护性 缺点:如果父类的属性发生变化,必须修改所有具体的子类对应的表 优点:只需修改一张表 优点:如果某个类的属性发生变化,只需修改和这个类对应的表
是否支持多态查询和多态关联 缺点:不支持 优点:支持 优点:支持
是否符合关系数据模型的常规设计规则 优点:符合 缺点:(1)在表中引入额外的区分子类的类型的字段(2)如果子类中的某个属性不允许为null,在表中无法为对应的字段创建not null约束 优点:符合
如果不需要支持多态查询和多态关联,可以采用每个具体类对应一个表的映射方式,如果需要支持多态查询和多态关联,并且子类包含的属性不多,可以采用根类对应一个表的映射方式,如果需要支持多态查询和多态关联,并且子类包含的属性很多,可以采用每个类对应一个表的映射方式。如果继承关系树中包含接口,可以把它当做抽象类来处理。
图14-9显示了一棵复杂的继承关系树,其中DOClass类为抽象类,其他均为具体类。

图14-9 复杂的继承关系树
可以将图14-9的继承关系树分解为3棵子树:
l DOClass类、ClassA类和ClassB类为一棵子树:DOClass类为抽象类,位于整个继承关系树的顶层,通常不会对它进行多态查询,因此可以采用每个具体类对应一个表的映射方式,ClassA类对应TABLE_A表,ClassB类对应TABLE_B表。
l ClassA类、ClassC类、ClassD类、ClassG类和ClassH类为一棵子树:ClassA类的所有子类都只包含少量属性,因此可以采用根类对应一个表的映射方式,ClassA类对应TABLE_A表。
l ClassB类、ClassE类和ClassF为一棵子树:ClassB类的两个子类都包含很多属性,因此采用每个类对应一个表的映射方式,ClassB类对应TABLE_B表,ClassE类对应TABLE_E表,ClassF类对应TABLE_F表。
如图14-10所示,在关系数据模型中,只需创建TABLE_A、TABLE_B、TABLE_E和TABLE_F表,其中TABLE_A中包含了与DOClass、ClassA、ClassC、ClassD、ClassG和ClassH的属性对应的字段。TABLE_B中包含了与DOClass和ClassB的属性对应的字段,TABLE_E和TABLE_F的B_ID字段既是主键,又是参照TABLE_B表的外键。








图14-10 复杂继承关系树对应的关系数据模型
只需创建两个映射文件,ClassA.hbm.xml和ClassB.hbm.xml,例程14-9和例程14-10分别为它们的源代码。
例程14-9 ClassA.hbm.xml
<hibernate-mapping >
<class name="mypack.ClassA" table="TABLE_A" discriminator-value="A" >
<id name="id" type="long" column="ID">
<generator class="increment"/>
</id>
<discriminator column="A_TYPE" type="string" />
<property name="a1" type="string" column="A1" />

<subclass name="mypack.ClassC" discriminator-value="C" >
<property name="c1" column="C1" type="string" />
</subclass>

<subclass name="mypack.ClassD" discriminator-value="D" >
<property name="d1" column="D1" type="string" />

<subclass name="mypack.ClassG" discriminator-value="G" >
<property name="g1" column="G1" type="string" />
</subclass>

<subclass name="mypack.ClassH" discriminator-value="H" >
<property name="h1" column="H1" type="string" />
</subclass>
</subclass>
</class>
</hibernate-mapping>

例程14-10 ClassB.hbm.xml
<hibernate-mapping >

<class name="mypack.ClassB" table="TABLE_B">
<id name="id" type="long" column="ID">
<generator class="increment"/>
</id>
<property name="b1" type="string" column="B1" />

<joined-subclass name="mypack.ClassE" table="TABLE_E">
<key column="B_ID" />
<property name="e1" column="E1" type="string" />
<property name="e2" column="E2" type="string" />
<property name="e3" column="E3" type="string" />
<property name="e4" column="E4" type="string" />
<property name="e5" column="E5" type="string" />
<property name="e6" column="E6" type="string" />
</joined-subclass >

<joined-subclass name="mypack.ClassF" table="TABLE_F">
<key column="B_ID" />
<property name="f1" column="F1" type="string" />
<property name="f2" column="F2" type="string" />
<property name="f3" column="F3" type="string" />
<property name="f4" column="F4" type="string" />
<property name="f5" column="F5" type="string" />
<property name="f6" column="F6" type="string" />
</joined-subclass >
</class>
</hibernate-mapping>
在ClassA.hbm.xml文件中,在用于映射ClassD的<subclass>元素中还嵌入了两个<subclass>元素,它们分别映射ClassG和ClassH类。在<class>以及所有的<subclass>元素中都设置了discriminator-value属性,Hibernate根据discriminator-value属性来判断TABLE_A表中的记录对应哪个类的实例,如果 TABLE_A表的一条记录的A_TYPE字段取值为“A”,表明它是ClassA类的实例,如果A_TYPE字段取值为“G”,表明它是ClassG类的实例,依次类推。
值得注意的是,在<subclass>元素中只能嵌入<subclass>子元素,但不能嵌入<joined-subclass>子元素,而在<joined-subclass>元素中只能嵌入<joined-subclass>子元素,但不能嵌入< subclass>子元素。
本节的范例程序位于配套光盘的sourcecode/chapter14/14.4目录下,运行该程序前,需要在SAMPLEDB数据库中手工创建TABLE_A表、TABLE_B表、TABLE_E表和TABLE_F表,相关的SQL脚本文件为/14.4/schema/sampledb.sql。
在DOS命令行下进入chapter14根目录,然后输入命令:
ant -file build4.xml run
就会运行BusinessService类。BusinessService的main()方法调用test()方法,test()方法调用saveDO(DOClass Object)方法,它负责保存一个DOClass对象,saveDO()方法的代码如下:
tx = session.beginTransaction();
session.save(object);
tx.commit();
在test()方法中,创建了一个ClassG类的实例和一个ClassF类的实例,然后调用saveDO()方法分别保存这两个实例:
ClassG g=new ClassG("a1","d1","g1");
saveDO(g);

ClassF f=new ClassF("b1","f1","f2","f3","f4","f5","f6","f7");
saveDO(f);
Session的save()方法能判断object变量实际引用的实例的类型,如果object变量引用ClassG类的实例,就执行如下insert语句:
insert into TABLE_A (ID,A1,D1,G1,A_TYPE) values (1, 'a1', 'd1', 'g1','G');
如果object变量引用ClassF类的实例,就执行如下insert语句:
insert into TABLE_B (ID,B1) values (1, 'b1');
insert into TABLE_F (B_ID ,F1, F2, F3, F4, F5, F6) values (1, 'f1', 'f2', 'f3',
'f4', 'f5', 'f6', 'f7');

14.5 映射多对一多态关联
Company与Employee类之间为一对多多态关联关系,如果继承关系树的根类对应一个表,或者每个类对应一个表,那么就能映射Company类的employees集合。本节介绍如何映射多对一多态关联。如图14-11所示,ClassD与ClassA为多对一多态关联关系。

图14-11 ClassD与ClassA为多对一多态关联关系
ClassA、ClassB和ClassC构成了一棵继承关系树,如果继承关系树的根类对应一个表,或者每个类对应一个表,那么可以按以下方式映射ClassD的a属性:
<many-to-one name="a"
class="ClassA"
column="A_ID"
cascade="save-update" />
假定与ClassD对应的表为TABLE_D,与ClassA对应的表为TABLE_A,在TABLE_D中定义了外键A_ID,它参照TABLE_A表的主键。
ClassD对象的a属性既可以引用ClassB对象,也可以引用ClassC对象,例如:
tx = session.beginTransaction();
ClassD d=(ClassD)session.get("ClassD",id);
ClassA a=d.getA();
if(a instanceof ClassB)
System.out.println(((ClassB)a).getB1());
if(a instanceof ClassC)
System.out.println(((ClassC)a).getC1());
tx.commit();
以下代码在映射ClassD类的a属性时使用了延迟检索策略:
<many-to-one name="a"
class="ClassA"
column="A_ID"
lazy="true"
cascade="save-update" />
当Hibernate加载ClassD对象时,它的属性a引用ClassA的代理类实例,在这种情况下,如果对ClassA的代理类实例进行类型转换,会抛出ClassCastException:
ClassA a=d.getA();
ClassB b=(ClassB)a; //抛出ClassCastException
解决以上问题的一种办法是使用Session.load()方法:
ClassA a=d.getA();
ClassB b=(ClassB)session.load(ClassB.class,a.getId());
System.out.println(b.getB1());
当执行Session的load()方法时,Hibernate并不会访问数据库,而是仅仅返回ClassB的代理类实例。这种解决办法的前提条件是必须事先知道ClassD对象实际上和ClassA的哪个子类的对象关联。
解决以上问题的另一种办法是显式使用迫切左外连接检索策略,避免Hibernate创建ClassA的代理类实例,而是直接创建ClassA的子类的实例:
tx = session.beginTransaction();
ClassD d=(ClassD)session.createCriteria(ClassD.class)
.add(Expression.eq("id",id))
.setFetchMode("a",FetchMode.EAGER)
.uniqueResult();
ClassA a=d.getA();
if(a instanceof ClassB)
System.out.println(((ClassB)a).getB1());
if(a instanceof ClassC)
System.out.println(((ClassC)a).getC1());
tx.commit();
如果继承关系树的具体类对应一个表,为了表达ClassD与ClassA的多态关联,需要在TABLE_D中定义两个字段:A_ID和A_TYPE,A_TYPE字段表示子类的类型,A_ID参照在子类对应的表中的主键。图14-12显示了表TABLE_D、TABLE_B和TABLE_C的结构。










图14-12 表TABLE_D、TABLE_B和TABLE_C的结构
由于关系数据模型不允许一个表的外键同时参照两个表的主键,因此无法对TABLE_D表的A_ID字段定义外键参照约束,而应该通过其他方式,如触发器,来保证A_ID字段的参照完整性。由于TABLE_D表的A_ID字段既可能参照TABLE_B表的ID主键,也可能参照TABLE_C表的ID主键,要求TABLE_B表和TALBE_C表的ID主键具有相同的SQL类型。
在ClassD.hbm.xml文件中,用<any>元素来映射ClassD的a属性:
<any name="a"
meta-type="string"
id-type="long"
cascade="save-update">
<meta-value value="B" class="ClassB" />
<meta-value value="C" class="ClassC" />
<column name="A_TYPE" />
<column name="A_ID" />
</any>
<any>元素的meta-type属性指定TABLE_D中A_TYPE字段的类型,id-type属性指定TABLE_D中A_ID字段的类型,<meta-value>子元素设定A_TYPE字段的可选值。在本例中,如果A_TYPE字段取值为“B”,表示为ClassB的对象,A_ID字段参照TABLE_B表中的ID主键;如果A_TYPE字段取值为“C”,表示为ClassC的对象,A_ID字段参照TABLE_C表中的ID主键。<column>子元素指定TABLE_D表中的A_TYPE字段和A_ID字段,必须先指定A_TYPE字段,再指定A_ID字段。
14.6 小 结
本章介绍了映射继承关系的三种方式:
l 继承关系树的每个具体类对应一个表:在具体类对应的表中,不仅包含和具体类的属性对应的字段,还包含和具体类的父类的属性对应的字段。这种映射方式不支持多态关联和多态查询。
l 继承关系树的根类对应一个表:在根类对应的表中,不仅包含和根类的属性对应的字段,还包含和所有子类的属性对应的字段。这种映射方式支持多态关联和多态查询,并且能获得最佳查询性能,缺点是需要对关系数据模型进行非常规设计,在数据库表中加入额外的区分各个子类的字段,此外,不能为所有子类的属性对应的字段定义not null约束。
l 继承关系树的每个类对应一个表:在每个类对应的表中只需包含和这个类本身的属性对应的字段,子类对应的表参照父类对应的表。这种映射方式支持多态关联和多态查询,而且符合关系数据模型的常规设计规则,缺点是它的查询性能不如第二种映射方式。在这种映射方式下,必须通过表的内连接或左外连接来实现多态查询和多态关联。
在默认情况下,对于简单的继承关系树可以采用根类对应一个表的映射方式。如果必须保证关系数据模型的数据完整性,可以采用每个类对应一个表的映射方式。对于复杂的继承关系树,可以将它分解为几棵子树,对每棵子树采用不同的映射方式。当然,在设计域模型时,应该尽量避免设计过分复杂的继承关系,这不仅会增加把域模型映射到关系数据模型的难度,而且也会增加在Java程序代码中操纵持久化对象的复杂度。
对于不同的映射方式,必须创建不同的关系数据模型和映射文件,但是域模型是一样的,域模型中的持久化类的实现也都一样。只要具备Java编程基础知识,就能创建具有继承关系的持久化类,因此本章没有详细介绍这些持久化类的创建过程,在此仅提醒一点,子类的完整构造方法不仅负责初始化子类本身的属性,还应该负责初始化从父类中继承的属性,例如以下是HourlyEmployee类的构造方法:
public class HourlyEmployee extends Employee{
private double rate;

/** 完整构造方法*/
public HourlyEmployee(String name, double rate,Company company) {
super(name,company);
this.rate=rate;
}

/** 默认构造方法*/
public HourlyEmployee() {}
……
}
Hibernate只会访问持久化类的默认构造方法,永远不会访问其他形式的构造方法。提供以上形式的完整构造方法,主要是为Java应用的编程提供方便。 
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值