使用 Apache OpenJPA 开发 EJB 3.0 应用,第 4 部分: 实体关联
对象和对象之间除了继承关系之外,还存在着关联关系:包括一对一、一对多、多对一和多对多关系,在 OpenJPA 框架下,开发者只需要使用 javax.persistence.OneToOne
这样的注释,并提供相应的配置内容,就可以轻松的实现实体之间的关联关系,并且能够实现实体的级联创建、更新和删除。
本文中我们将以实体之间的一对一关联关系为例,深入地讲述如何使用 OpenJPA 框架提供的注释,实现企业应用中实体之间的关联关系。文中将提供一个简单的例子,详细的说明如何定义类和类之间的一对一关联关系的步骤,同时会重点讲述这些注释所支持的属性。一对多、多对一和多对多这三种关联关系在 OpenJPA 中的实现过程和一对一关联关系的实现过程是一致的,只是需要选择使用不同的注释,在本文的最后,会对实现这三种关联关系进行简单说明,读者可以参考一对一关系的实现过程来实现一对多、多对一和多对多的关联关系。
一对一关系
在面向对象的世界里,类 A 和类 B 之间形成一对一关系必须满足如下条件:
- 对象 A1 引用了对象 B1;
- 类 A 的其它对象 An 不能引用同样的对象 B1。
在关系数据库中,我们通常使用唯一外键的方式来实现一对一关系,下面这个图说明了这种的情况。
图 1. 关系数据库中的一对一关系
下面开始介绍 OpenJPA 中实现实体之间一对一关联关系的相关知识,为了说明的需要,我们首先定义一个简单的应用场景。
模拟场景
假定开发者要完成一个图书馆管理系统,我们需要记录书的基本信息如编号、书名、出版日期等基本信息,还需要记录书的前言,序等信息。
为了说明实体之间的一对一关系,我们将书设计成一个类(Book),包括书的编号和名称两个属性,同时将书的前言设计成另外一个类(BookExtend),它包括书的编号和前言两个属性。由于一本书有前言而且也不可能有其它书的前言部分会和它一样,所以类 Book 和 BookExtend 之间很自然的形成了一对一的关系。这两个类的属性以及类之间的关系如下图所示。
图 2. 类之间的一对一关系
[注]:为了说明的简单,本例子设计时每个对象只选择了必要的属性。
描述一对一关系
在 OpenJPA 中,开发者用来描述实体之间一对一关系时可选择的注释包括 javax.persistence.OneToOne
和javax.persistence.JoinColumn
。其中 javax.persistence.OneToOne
注释是必须使用的,它被用来声明类和类之间存在着一对一关系,javax.persistence.JoinColumn
注释是可选的,开发者使用 JoinColumn
注释来声明两个类在数据库中对应的表之间关联时的细节,包括主表中关联字段的名称、从表中使用什么字段来进行关联等。
javax.persistence.OneToOne
javax.persistence.OneToOne
注释支持如下 5 个属性,它们可以被开发者用来定义实体和实体之间一对一关联关系的细节内容。
target Entity
targetEntity
属性是 Class 类型的属性。定义实体一对一关系中处于从属地位的实体类的类型。如果没有为该属性设置值,OpenJPA 容器默认 targetEntity
属性的值是该成员属性对应的类类型,所以实体关系定义时通常不需要为 targetEntity
属性设置值。
mappedBy
mappedBy
属性是 String 类型的属性。mappedBy
属性的值是当前实体在关联实体中的属性名称,使用 mappedBy
可以定义实体类之间的双向关系。如果类之间是单向关系,不需要提供定义,如果类和类之间形成双向关系,我们就需要使用这个属性进行定义,否则可能引起数据一致性的问题。
以演示场景中 Book 和 BookExtend 实体为例,假设我们只定义 Book 类有 BookExtend 类型的属性,而 BookExtend 并没有 Book 类型的属性,那么说明 Book 和 BookExtend 实体之间是单向关系;如果 BookExtend 中也定义了 Book 属性,那么 Book 和 BookExtend 实体之间就构成了双向关系。
cascade
cascade
属性的类型是 CascadeType[]
类型。cascade
属性定义实体和实体之间的级联关系。使用 cascade
属性定义的级联关系将被容器视为对当前类对象及其关联类对象采取相同的操作,而且这种关系是递归调用的。
以演示场景中 Book 和 BookExtend 实体为例:如果设置 Book 和 BookExtend 存在级联关系,那么删除 Book 时将同时删除它所对应的 BookExtend 对象。而如果 BookExtend 还和其它的对象之间有级联关系,那么这样的操作会一直递归执行下去。
cascade
的值只能从 CascadeType.PERSIST
(级联新建)、CascadeType.REMOVE
(级联删除)、CascadeType.REFRESH
(级联刷新)、CascadeType.MERGE
(级联更新)中选择一个或多个。还有一个更方便的选择是使用 CascadeType.ALL
,表示选择上面全部四项。
fetch
fetch
属性是 FetchType
类型的属性。可选择项包括:FetchType.EAGER
和 FetchType.LAZY
。前者表示关联关系的从类在主类加载的时候同时加载,后者表示关联关系的从类在自己被访问时才加载。默认值是 FetchType.EAGER
。
optional
optional
属性是 boolean 类型的属性。optional
属性用于定义关联关系的从类对象是否必须存在。如果设置为 false,那么该属性就不能设置为 null。默认值是 true。
javax.persistence.OneToOne 用法举例
public class Book {
// 其它实体映射内容…
/*
* 使用 OneToOne 注释表示该属性和 Book 类形成一对一关系,OneToOne
* 注释的 option 属性设为 True 表示该对象可以不存在,cascade 属性
* 设置为 CascadeType.ALL,表示 Book 和 BookExtend 对象级联新建、 更新、删除、刷新
*/
@OneToOne(optional=true,cascade=CascadeType.ALL)
public BookExtend bookExtend;
}
javax.persistence.JoinColumn
javax.persistence.JoinColumn
注释可以和 javax.persistence.OneToOne
注释一起使用,用于定义关联关系中的主类在数据库中对应的表通过什么字段和关联关系中的从类的主键进行关联,这个注释是可选的,如果不提供该注释,OpenJPA 会默认使用”对象名_ID”和关联表的主键字段进行关联。
以演示场景中 Book 和 BookExtend 实体为例:如果 Book 的 bookExtend 属性没有使用 javax.persistence.JoinColumn
注释进行声明,我们使用 OpenJPA 提供的 Mapping Tool 工具生成表格的时候,Book 类对应的表 Book 中将自动加入列 bookExtend_ID,它的类型将和 BookExtend 对应表的主键字段id类型保持一致。
JoinColumn
注释支持两个重要属性:name
和 referencedColumnName
属性。
name
name
属性的类型是 String 类型。name
属性用于指定关联关系中的主类对应的表中和关联关系中的从类的主键进行关联的字段的名称。以演示场景中 Book 和 BookExtend 实体的关系为例:如果 Book 实体对应的表使用“beID”字段和 BookExtend 实体对应表的主键进行对应,我们可以在 Book 类中为 bookExtend 属性提供 javax.persistence.JoinColumn
注释,设置它的 name 属性为“beID”。
referencedColumnName
referencedColumnName
属性的类型是 String 类型。referencedColumnName
属性指定关联关系中的从类与关联关系中的主类对应的表之间形成关联关系的字段名称,通常用于关联关系中的从类的关联字段不是自己的主键的情况。以演示场景中 Book 和 BookExtend 实体的关系为例:BookExtend 表中默认使用 Id 字段和 Book 类的某个字段进行关联,但如果实际情况下 BookExtends 表需要使用“myID“字段和 Book 表进行关联,我们就可以设置 javax.persistence.JoinColumn
注释的属性值为“myID”。
javax.persistence.JoinColumn 用法举例
public class Book {
// 其它内容…
/*
* 使用 OneToOne 注释表示该属性和 Book 类形成一对一关系,OneToOne
* 注释的 option 属性设为 True 表示该对象可以不存在,cascade 属性
* 设置为 CascadeType.ALL,表示 Book 和 BookExtend 对象级联新建、 更新、删除、刷新
*/
@OneToOne(optional = true, cascade = CascadeType.ALL)
/*
* 使用 JoinColumn 注释设置两个对象对应数据库表之间的关联字段
* name 属性指定关联关系中主类对应表中参与关联关系的字段名称,
* referencedColumnNam 属性指定关联关系中从类对应表中参与关
* 联关系的字段名称,
*/
@JoinColumn(name = "beID", referencedColumnName = "myID")
public BookExtend bookExtend;
}
编写符合要求的持久化类
根据模拟场景的需求,结合我们前面学习到的描述实体之间一对一关联关系的知识,我们可以采用如下设计:
- Book 类和 BookExtend 之间存在一对一关联关系;
- Book、BookExtend 对应的表的主键字段由 MySQL 自动生成;
- Book 表中参与关联关系的字段名为“beID“;
- BookExtend 表中参与关联关系的字段使用默认字段“ID”;
- Book 类和 BookExtend 类之间存在全部级联关系;
- 不是每一个 Book 对象都需要有对应的 BookExtend 对象。
根据这样的设计,我们可以开始编写实体 Book 和 BookExtend 对应的持久化类代码,下面是作者编写的两个实体类的全部代码,大家可以参考代码中加入的大量注释学习如何使用注释来描述实体和实体之间的一对一关联关系。
Book 类
package org.vivianj.openjpa.beans;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
/**
* Book 用于表征系统中的书籍对象,它有三个属性 id - 书籍编号, 书籍编号将由 MySQL 数据库自动生成 name - 书名 bookExtend
* – 书的扩展信息,和 BookExtend 是一对一(OneToOne)关系
*/
@Entity(name = "Book")
public class Book {
/* Id 注释表示该字段是标识字段 */
@Id
/*
* GeneratedValue 注释定义了该标识字段的产生方式,我们的演示系统中 id 由 MySQL 数据库字段自动生成,因此选择
* GenerationType.IDENTITY
*/
@GeneratedValue(strategy = GenerationType.IDENTITY)
/*
* Column 注释的 name 属性定义了该类属性对应的数据字段的名称, 为了最大限度保持系统和数据库之前的独立性,建议使用大写字符
*/
@Column(name = "ID")
public int id;
/* Basic 注释表示该属性是基本属性 */
@Basic
/*
* Column 注释的 name 属性定义了该类属性对应的数据字段的名称, 为了最大限度保持系统和数据库之前的独立性,建议使用大写字符
*/
@Column(name = "NAME")
public String name = null;
/*
* 使用 OneToOne 注释表示该属性和 Book 类形成一对一关系,OneToOne 注释的 option 属性设为 True
* 表示该对象可以不存在,cascade 属性 设置为 CascadeType.ALL,表示 Book 和 BookExtend 对象级联新建、
* 更新、删除、刷新
*/
@OneToOne(optional = true, cascade = CascadeType.ALL)
/* 使用 JoinColumn 注释设置两个对象对应数据库表之间的关联字段 */
@JoinColumn(name = "extendID")
public BookExtend bookExtend;
}
BookExtend 类
package org.vivianj.openjpa.beans;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
/**
* BookExtend 用于表征系统中书的扩展信息,它有两个属性: id - 扩展信息编号,扩展信息编号将由 MySQL 数据库自动生成 name
* -书的前言信息
*/
@Entity
public class BookExtend {
/* Id 注释表示该字段是标识字段 */
@Id
/*
* GeneratedValue 注释定义了该标识字段的产生方式,我们的演示系统中 id 由 MySQL 数据库字段自动生成,因此选择
* GenerationType.IDENTITY
*/
@GeneratedValue(strategy = GenerationType.IDENTITY)
/*
* Column 注释的 name 属性定义了该类属性对应的数据字段的名称, 为了最大限度保持系统和数据库之前的独立性,建议使用大写字符
*/
@Column(name = "ID")
public int id;
/* Basic 注释表示该属性是基本属性 */
@Basic
/*
* Column 注释的 name 属性定义了该类属性对应的数据字段的名称, 为了最大限度保持系统和数据库之前的独立性,建议使用大写字符
*/
@Column(name = "NAME")
public String name = null;
}
调用代码
上面的代码中,我们已经准备好了符合要求的持久化类,下面我们看看 OpenJPA 中如何调用这两个类完成 Book 类和 BookExtend 类的创建、修改、删除工作。
由于篇幅的关系,这些没有讲述如何编译、加强这些类并且准备相应的配置文件来完成整个项目开发环境的建立,这部分的内容请参考另外一篇文章《OpenJPA:符合 EJB3 规范的持久层框架》。
级联新建对象
下面的这段代码演示了只需要调用 Book 类的 persist 方法就同时持久化 Book 类对象和 BookExtend 类对象的情况。请注意其中用粗体标识出的部分。
/* 获得 EJB 的实体管理器 */
EntityManagerFactory emf = Persistence.createEntityManagerFactory(null);
EntityManager em = emf.createEntityManager(PersistenceContextType.EXTENDED);
/* 开始事务 */
em.getTransaction().begin();
/* 创建新的 BookExtend 对象 */
BookExtend bookExtend = new BookExtend();
/* 设置对象属性 */
bookExtend.name = "前言 本书重点说明了...";
/* 创建新的 Book 对象 */
Book book = new Book();
/* 设置 Book 对象的 name 属性 */
book.name = "<<Web Services实践>>";
/* 建立对象之间的关系 */
book.bookExtend = bookExtend;
/* 持久化对象,只需要持久化 Book 对象,不需要单独持久化 bookExtend 对象 */
em.persist(book);
/* 结束事务 */
em.getTransaction().commit();
em.close();
emf.close();
产生的 SQL 语句
下面的这段 SQL 语句是运行上面的代码时 OpenJPA 自动生成的,我们可以从中看到 OpenJPA 级联新建对象时的处理过程:
-- 创建 Book 实体对应的数据库表 --
CREATE TABLE Book (ID INTEGER NOT NULL AUTO_INCREMENT, NME VARCHAR(255),extendID INTEGER, PRIMARY KEY (ID));
-- 创建 BookExtend 实体对应数据库表 --
CREATE TABLE BookExtend (ID INTEGER NOT NULL AUTO_INCREMET,NAME VARCHAR(255), PRIMARY KEY (ID)) ;
-- 将 Book 实体对象插入数据库中 --
INSERT INTO Book (NAME) VALUES ('<<Web Services实践>>')
-- 获取 Book 实体对象的编号 --
SELECT LAST_INSERT_ID();
-- 将 BookExtend 实体对象插入数据库中 --
INSERT INTO BookExtend (NAME) VALUES ('前言 本书重点说明了...') ;
--获取 BookExtend 实体对象的编号 --
SELECT LAST_INSERT_ID();
-- 将 BookExtend 实体对象的编号更新到Book表中形成关联关系 --
UPDATE Book SET extendID = 1 WHERE ID = 1;
级联更新对象状态
下面的这段代码演示了只需要调用 Book 类的 merge 方法就同时更新 Book 类对象和 BookExtend 类对象状态的情况。请注意其中用粗体标识出的部分。
/* 获得 EJB 的实体管理器 */
EntityManagerFactory emf = Persistence.createEntityManagerFactory(null);
EntityManager em = emf.createEntityManager(PersistenceContextType.EXTENDED);
/* 开始事务 */
em.getTransaction().begin();
/* 创建新的 Book 对象 */
Book book = new Book();
/* 设置 Book 对象的 id 属性 */
book.id= 1;
book.name = “OpenJPA入门”;
/* 创建新的 BookExtend 对象 */
BookExtend bookExtend = new BookExtend();
/* 设置对象属性 */
bookExtend.id=1;
bookExtend.name = "OpenJPA开发EJB3.0应用 ...";
/* 建立对象之间的关系 */
book.bookExtend = bookExtend;
/* 持久化对象,只需要调用 Book 对象的 merge 方法,不需要单独处理 bookExtend 对象 */
em.merge(book);
/* 结束事务 */
em.getTransaction().commit();
em.close();
emf.close();
级联删除对象
下面的这段代码演示了只需要通过 Query 对象,就可以在删除 Book 类的同时,删除它对应的 BookExtend 实体对象的情况。请注意其中用粗体标识出的部分。
/* 获得 EJB 的实体管理器 */
EntityManagerFactory emf = Persistence.createEntityManagerFactory(null);
EntityManager em = emf.createEntityManager(PersistenceContextType.EXTENDED);
/* 开始事务 */
em.getTransaction().begin();
/* 使用查询删除对象,可以不必将对象加入到内存中,提高效率 */
Query q = entityManager.createQuery("delete from Book c WHERE c.id=:id");
int id = book.id;
/* 设置被删除 Book 对象的主键值 */
q.setParameter("id", id);
/* 当方法被调用时,Book 对象对应的 BookExtend 对象会同时被删除 */
q.executeUpdate();
/* 结束事务 */
em.getTransaction().commit();
em.close();
emf.close();
其它几种关联关系
在上面的文章中我们学习了如何在 OpenJPA 中通过 javax.persistence.OneToOne 注释和 javax.persistence.JoinColumn 注释实现实体之间的一对一关联关系。在企业应用中,除了一对一关联关系,实体之间还可能存在一对多、多对一、多对多等关联关系,不过在 OpenJPA 容器中,这些实体之间关联关系的实现都大同小异,只是需要开发者选择使用不同的注释。
开发者用来描述实体之间一对多关联关系的注释是 javax.persistence.OneToMany 注释,用来描述实体之间多对一关联关系的注释是 javax.persistence.OneToMany 注释,用来描述实体之间多对多关联关系的注释是 javax.persistence.ManyToMany 注释。这三个注释都支持 targetEntity、mappedBy、cascade 和 fetch 这四个属性,这些属性的具体含义和 OneToOne 注释注释的同名属性一一对应,请大家参考前面章节中的内容。
javax.persistence.OneToMany、javax.persistence.OneToMany、javax.persistence.ManyToMany 这三个注释都可以和 javax.persistence.JoinColumns 注释一起使用,javax.persistence.JoinColumns 注释的作用是为一对多、多对一、多对多关联关系在数据库中的体现提供更多细节描述。javax.persistence.JoinColumns 注释中可以包含多个 javax.persistence.JoinColumn 注释的内容,javax.persistence.JoinColumn 注释的属性请参考本文前面部分的描述。
总结
对象和对象之间除了继承关系之外,还存在着关联关系,包括一对一、一对多、多对一和多对多的关系,本文中,作者以实体之间的一对一关联关系为例,结合企业应用中的实际例子,详细地描述了如何在 OpenJPA 框架下通过注释简单的描述实体和实体之间的关联关系,并且实现实体的级联操作。文章的最后简单的介绍了 OpenJPA 中实现实体之间一对多、多对一和多对多关联关系时需要用到的注释,这些注释的用法和描述一对一关联关系时大体一致,如何通过描述实现实体之间一对多、多对一和多对多关联关系请大家参考本文中的内容自行完成。