面向对象和关系型数据库
Java 语言天生就是一门面向对象的编程语言,在 Java 世界中,被处理的内容都被组织成一个一个的对象,对象和对象之间存在着继承、引用关系,这样的关系无法通过简单的方式直接映射到关系型数据库中。因此在关系型数据库与面向对象之间存在着阻抗失谐。
我们通过一个简单的例子来说明这种阻抗失谐给企业应用开发者带来的困难。假设在企业应用中存在三个 Java 类:Animal、Fish 和 Dog。Animal 仅仅具备两个属性:id 和 name。Fish 是一种 Animal,但是人们比较关注它的生活区域是在海里还是在河里,因此它除了继承自 Animal 之外,还有自己独特的属性 territory。Dog 也是一种 Animal,人们比较关注它的性别,因此它除了继承自 Animal 之外,还有自己独特的属性 sex。我们可以用下面这个图描述三者之间的关系模型。
图 1. Animal、Fish 和 Dog 对象模型
在 Java 应用中,由于动态绑定的支持,Fish、Dog 都可以被作为 Animal 对象处理。但是如果我们换到关系型数据库的视角,情况发生了改变: 通常情况下,Animal、Fish、Dog 毫无关联,它们都保存在各自对应的表中,假设分别对应 Animal 表、Fish 表和 Dog 表,如果要维护 Animal 和 Fish 的继承关系,我们就需要使用 SQL 的联合查询语句查出 Animal 的所有属性和 Fish 的所有属性,这样就必须使用某种外键进行关联:
Select animal.*,fish.* form animal,fish where animal.id = fish.id
从这个简单的例子中我们就可以看出,一个企业应用开发者使用 Java 实现企业应用时需要同时掌握面向对象和关系型数据库的两种思想,而且还必须保证它们之间的映射是正确的,否则无法保证企业应用的正确性,这对于企业应用开发者是个的挑战,因此 Java 社区一直在寻求如何将面向对象和关系型数据库思想统一起来的简单途径,这方面的努力促进了持久化技术的发展。
OpenJPA 是最新的尝试,它能够将对象继承关系的持久化透明化,企业应用开发者仅需要处理对象模型,而不需要处理和关系型数据库有关的内容,极大地降低了对象继承关系持久化的难度。下面我们来了解 OpenJPA 中持久化对象继承关系的几种方式。
持久化对象继承关系的方式
我们从关系数据库角度看对象继承关系的持久化这个问题域:对象继承通常意味着子类比父类提供更多的属性,持久化对象继承关系的实质就是如何根据对象的类型动态的处理这些多出来的属性。OpenJPA 框架支持使用三种不同的策略处理对象继承关系:
1. 类及其子类保存在一张数据库表中
在这种情况下,类及其子类都保存在同一张数据表中,该表提供足够的字段保存类及其子类的所有属性,同时提供一个特别字段保存当前记录对应类的实际类名(默认名 DTYPE,也可以在开发时指定其它名称)。在企业应用运行过程中,OpenJPA 框架根据 Java 对象的实际类型和数据库表进行绑定。
以上一章节中提到的对象模型为例: Animal、Fish、Dog 三个类的所有对象实例都被保存在 Animal 数据表中,该表将会有 5 个属性,其中 ID,NAME 字段对应 ANIMAL 类的两个属性,ID、NAME、SEX 对应 Dog 类的属性,ID、NAME、STERRITORY 对应 Fish 类的属性。DTYPE 是 OpenJPA 加入的字段,用于确定当前记录的实际类类型,在这里例子中,它的内容是“ANIMAL”、“FISH”或者是“DOG”。
图 2. 第一种策略的数据库表现
2. 类和子类分别保存在不同的数据库表中,互相之间没有关联
这种情况下,开发者不理会类之间是否存在继承关系,为每一个类的持久化使用唯一的表,父类对象保存在父类对应的表中,子类对象的信息保存在子类对应的表中,这也是通常的持久化框架采用的方式。下面这个图显示了这种情况下对象继承关系数据库中的表现。
以上一章节中提到的对象模型为例: Animal、Fish、Dog 三个类的对象实例都被保存在各自对应的数据表中。下面这个图显示了这种情况下对象继承关系数据库中的表现。
图 3. 第二种策略的数据库表现
3. 类和子类分别保存在不同的数据库表中,子类中不保存父类中已有的属性,仅通过主键进行关联
这种情况下,父类和子类对应不同的表,但是子类对应的表中不再保存父类对应表中已经存在的字段信息,两个表之间通过关键字段关联起来,也就是数据库技术中通常所说的外健。这种实现方式是最理想化的一种,既能够处理对象之间的继承,又满足了关系数据库中对于设计范式的要求。
以上一章节中提到的对象模型为例: Animal、Fish、Dog 三个类的对象实例都被在 Animal 表中有记录;而 Fish 对象的 TERRITORY 属性者被 FISH 表所保存,FISH 表通过 ID 和 Animal 表中的数据进行关联;而 Dog 对象的 SEX 属性者被 Dog 表所保存,Dog 表通过 ID 和 Animal 表中的数据进行关联。下面这个图显示了这种情况下对象继承关系数据库中的表现。
图 4. 第三种策略的数据库表现
这三种方式的处理对于开发者而言是透明的,无论选择哪一种,仅仅影响数据在关系数据库中的保存方式,对于开发者而言,只需要按照面向对象的方式操作对象既可,OpenJPA 框架在处理持久化操作的时候,会动态地判断当前对象的实际类类型(后期绑定),从而确定持久化到哪个表中。在一个企业应用的实现中,开发者可以根据需要选择这三种方式的一种或者几种来处理对象之间的继承关系。
Inheritance 注释
OpenJPA 是一个基于注释的持久化框架,对持久化的大多数元信息都只需要为实体类提供相应的注释。开发者使用注释描述实体和数据库表之间的映射,也采用注释描述对象继承关系的持久化。javax.persistence.Inheritance 注释用来指定对象继承关系持久化的方式。它的 strategy 属性用于指定持久化对象继承关系在关系数据库中的表现形式,可选择项包括 SINGLE_TABLE、JOINED 和 TABLE_PER_CLASS。它们三个都是 javax.persistence.InheritanceType 中定义的常量。
- SINGLE_TABLE
strategy 设置为 SINGLE_TABLE 选项表示所有类及其子类保存在同一个数据库表中,对象的类型使用表中的特殊字段 DTYPE 进行识别。
- TABLE_PER_CLASS
strategy 设置为该选项表示每个类使用一个表。
- JOINED
strategy 设置为该选项表示父类和子类分别保存在不同的数据库表中,子类中不保存父类对应数据库表中已有的属性,仅通过主键进行关联。
javax.persistence.Inheritance 注释是类级别的注释。需要为每一个成为父类的实体类提供 javax.persistence.Inheritance 注释并且指定 strategy 属性。在同一个企业应用中,开发者可以根据实际情况选择这三种策略中的一种,或者是几种同时使用。
对象继承关系的持久化和查询
上面的章节中,我们已经介绍了 OpenJPA 中处理对象继承的方法,下面我们通过一些简短的代码来演示如何实现 Animal、Fish、Dog 及其继承关系的持久化,同时介绍如何将这种对象继承关系从数据库中还原出来。
演示中,我们选择使用实现第三种方式:JOINED。这也是 OpenJPA 中持久化对象继承的最佳实践,既符合 Java 开发者面向对象的习惯,也能够符合关系数据库设计的范式要求,而且数据库中的数据冗余最小。TABLE_PER_CLASS 和 SINGLE_TABLE 方式下的对象继承关系持久化的例子请读者参考下面的步骤自己完成。
实体类 Animal
首先我们创建实体类 Animal,它是 Fish 和 Dog 的父类。因此必须为它处理提供 javax.persistence.Inheritance 注释。我们选择使用 JOINED 策略处理继承关系,因此设置 javax.persistence.Inheritance 注释的 strategy 属性为 InheritanceType.JOINED。
清单 1 Animal.java, 继承关系的父类
package chapter04.entity;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Animal {
@Id
private int id;
private String name;
public Animal() {
}
public Animal(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
实体类 Fish
实体类 Fish 是 Animal 的子类,因此它必须继承自 Animal。同时根据 OpenJPA 的要求,每一个实体必须使用 javax.persistence.Entity 进行注释,因此在 Fish 类声明之前提供 javax.persistence.Entity 注释。
清单 2 Fish.java, 继承关系中的子类
package chapter04.entity;
import javax.persistence.Entity;
@Entity
public class Fish extends Animal {
/* 鱼的活动范围,比如江、河、湖、海 */
private String territory;
public Fish() {
}
public Fish(int id, String name, String territory) {
super(id, name);
this.territory = territory;
}
public String getTerritory() {
return territory;
}
public void setTerritory(String territory) {
this.territory = territory;
}
}
实体类 Dog
实体类 Dog 是 Animal 的子类,因此它必须继承自 Animal。和 Fish 类一样,我们需要在 Dog 类声明之前提供 javax.persistence.Entity 注释。
清单 3 Dog.java, 继承关系中的子类
package chapter04.entity;
import javax.persistence.Entity;
@Entity
public class Dog extends Animal {
/* 性别 */
private String sex;
public Dog() {
}
public Dog(int id, String name, String sex) {
super(id, name);
this.sex = sex;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
创建合适的数据库表
我们可以使用下面的语句创建数据库表:
CREATE TABLE ANIMAL(ID INTEGER NOT NULL PRIMARY KEY,NAME VARCHAR(255))
CREATE TABLE DOG(ID INTEGER NOT NULL PRIMARY KEY,SEX VARCHAR(255))
CREATE TABLE FISH(ID INTEGER NOT NULL PRIMARY KEY,TERRITORY VARCHAR(255))
[注] 使用 OpenJPA 中的 MappingTool 工具可以很容易的保持 Entity 和数据库之间的一致性,也可以使用 MappingTool 工具生成的数据库定义文件(DDL)创建应用正常运行所需要的数据库结构。请参考 OpenJPA 的帮助文档中关于 MappingTool 部分的内容。
持久化实体
使用 OpenJPA 持久化实体的继承关系时,开发者只需要按照面向对象的思想操纵实体即可,无需为实体的继承关系作多余的工作。下面的章节中我们将了解持久化实体 Animal、Fish、Dog 时开发者需要完成的工作以及 OpenJPA 转化后在关系数据库中的实现细节。
持久化 Animal
我们可以使用下面的代码段来持久化一个新的 Animal 对象:
// 通过 Persistence 创建 EntityManagerFactory
EntityManagerFactory factory = Persistence.createEntityManagerFactory("jpa-unit", System.getProperties());
// 从 EntityManagerFactory 中创建 EntityManager
EntityManager em = factory.createEntityManager();
// 开始持久化实体的事务
em.getTransaction().begin();
// 使用相同的方式持久化实体
em.persist(new Animal(1,"honey"));
// 提交持久化实体的事务
em.getTransaction().commit();
// 关闭EntityManager
em.close();
当我们执行这段代码时,OpenJPA 会将它转化为关系数据库对应的 SQL 语句:
INSERT INTO Animal (id, name) VALUES (1, 'honey')
[注] 如果您还不知道如何使用 OpenJPA 持久化对象,请阅读 本系列 前面的文章,了解 OpenJPA 开发的基本知识。
持久化 Fish
Fish 对象的持久化和 Animal 实体的持久化过程没有任何的不同,只不过 persist 方法的参数变成了 Fish 对象。我们可以使用下面的代码段来持久化一个新的 Fish 对象,请注意下面代码中加粗的部分。
// 通过 Persistence 创建 EntityManagerFactory
EntityManagerFactory factory = Persistence.createEntityManagerFactory("jpa-unit", System.getProperties());
// 从 EntityManagerFactory 中创建 EntityManager
EntityManager em = factory.createEntityManager();
// 开始持久化实体的事务
em.getTransaction().begin();
// 使用相同的方式持久化实体
em.persist(new Fish(2,"mermaid","SEA"));
// 提交持久化实体的事务
em.getTransaction().commit();
// 关闭EntityManager
em.close();
由于 Fish 对象的属性保存在两个表中,因此当我们执行这段代码时,OpenJPA 会将它转化为对应的两条 SQL 语句:
INSERT INTO Animal (id, name) VALUES (2, 'mermaid')
INSERT INTO Fish (id, territory) VALUES (2, 'SEA')
持久化 Dog
持久化 Dog 对象和持久化 Fish 对象的过程几乎一样,区别是 persist 方法的参数变成了 Dog 对象。
em.persist(new Dog(3,"ba guai","MALE"));
和持久化 Fish 对象时一样,Dog 对象的属性也保存在两个表中,因此当我们执行这段代码时,OpenJPA 会将它转化为对应的两条 SQL 语句:
INSERT INTO Animal (id, name) VALUES (3, 'ba guai')
INSERT INTO Dog (id, sex) VALUES (3, 'MALE')
从数据库中查询实体对象
在上一章节中我们了解了如何持久化存在继承关系的实体内,并且介绍了 OpenJPA 在处理继承关系时的细节行为,接下来我们将介绍如何从数据库中获取实体,以及 OpenJPA 在这个过程中对于继承关系处理的细节。
获取所有 Animal 对象
我们通过 OpenJPA 中的 Query 接口和 JPQL(Java Persistence Query Language)语言来获取数据库中的记录并且转换为相应的 Java 对象,因此开发者只需要处理 Java 对象模型即可。下面的代码可以从数据库中获取所有的 Animal 对象,请注意其中粗体的部分。
// 通过 Persistence 创建 EntityManagerFactory
EntityManagerFactory factory = Persistence.createEntityManagerFactory("jpa-unit", System.getProperties());
// 创建新的 EntityManager
EntityManager em2 = factory.createEntityManager();
// 查询所有 Animal 对象
Query q = em2.createQuery("select m from Animal m");
// 直接处理 Animal 对象,打印 Animal 对象的信息
for (Animal m : (List<Animal>) q.getResultList()) {
System.out.println("Animal Object:");
System.out.println(" id:" + m.getId());
System.out.println(" name:" + m.getName());
}
// 关闭 EntityManager 和 EntityManagerFactory
em2.close();
factory.close();
当我们执行这段代码时,OpenJPA 会将它转化为关系数据库对应的 SQL 查询语句:
SELECT t0.id, t1.id, t2.id, t0.name, t1.sex, t2.territory FROM Animal t0 LEFT OUTER JOIN Dog t1 ON t0.id = t1.id LEFT OUTER JOIN Fish t2 ON t0.id = t2.id
在查询结果返回后,OpenJPA 会将查询结果影射到相关的 Animal 对象上,整个过程是透明的,开发者只需要处理对象模型即可。
获取所有 Fish 对象
Fish 对象的获取和 Animal 对象的获取在 OpenJPA 中大同小异,唯一的区别是使用 JPQL 不相同,查询 Fish 对象时使用“select fish from Fish fish
”。下面的代码可以从数据库中获取所有的 Fish 对象,请注意其中粗体的部分。
// 通过 Persistence 创建 EntityManagerFactory
EntityManagerFactory factory = Persistence.createEntityManagerFactory("jpa-unit", System.getProperties());
// 创建新的 EntityManager
EntityManager em2 = factory.createEntityManager();
// 查询所有 Fish 对象
Query q1 = em2.createQuery("select fish from Fish fish");
// 打印 Fish 对象的信息
for (Fish fish : (List<Fish>) q1.getResultList()) {
System.out.println("Fish Object:");
System.out.println(" id:" + fish.getId());
System.out.println(" name:" + fish.getName());
System.out.println(" territory:" + fish.getTerritory());
}
// 关闭 EntityManager 和 EntityManagerFactory
em2.close();
factory.close();
当我们执行这段代码时,OpenJPA 会将它转化为关系数据库对应的 SQL 查询语句:
SELECT t1.id, t0.id, t1.name, t0.territory FROM Fish t0 INNER JOIN Animal t1 ON t0.id = t1.id
在查询结果返回后,OpenJPA 会将查询结果影射到相关的Fish对象上,整个过程是透明的,开发者只需要处理对象模型即可。
获取所有 Dog 对象
获取 Dog 对象的过程和获取 Fish 对象的过程一致,开发者只需要将 Query 接口使用的 JPQL 语句改为"select dog from Dog dog
"。
Query q1 = em2.createQuery("select dog from Dog dog ");
当我们执行这段代码时,OpenJPA 会将它转化为关系数据库对应的 SQL 查询语句:
SELECT t1.id, t0.id, t1.name, t0.sex FROM Dog t0 INNER JOIN Animal t1 ON t0.id = t1.id
在查询结果返回后,OpenJPA 会将查询结果影射到相关的Fish对象上,整个过程是透明的,开发者只需要处理对象模型即可。
总结
对象继承关系在关系数据库中的表现是对象持久化中难于实现的部分,OpenJPA 为开发者提供了一种透明的实现。在 OpenJPA 中提供了 SINGLE_TABLE、JOINED 和 TABLE_PER_CLASS 三种实现方式处理实体继承关系,开发者需要做的仅仅是为实体类提供 javax.persistence.Inheritance 注释,同时设置它的 strategy 属性,确定使用哪种对象继承关系即可,和关系数据库交互的部分由 OpenJPA 框架完成。
在 下一期 文章中,让我们来了解如何使用 OpenJPA 处理实体之间的关联关系,如一对一、一对多、多对一、多对多关系,我们将介绍相关的注释,并学习如何在代码中使用它,敬请期待!