JPA 标准提供了许多通用的 annotation 进行 OR Mapping。但实践表明已有的 annotation 常常不能满足需求,本文将结合 OpenJPA,介绍一些扩展的常用 annotation 以更好的满足 OR Mapping 需求。
JPA 和 OpenJPA 简介
随着面向对象的数据模型被广泛应用,将面向对象的实体映射为关系数据库表项(OR Mapping)已经越来越多的出现在各类应用程序的开发设计之中。JPA(Java Persistence API)是 Sun Microssystems 公司制定的一个把 Java 数据对象映射成关系数据库对象的一个标准。JPA 弥补了 JDBC、ORM、EJB2 等在 Java 对象持久化方面的不足之处,并且非常易于使用。JPA 因大量使用了像 annotations 和 generics 等 Java 语言的新特性,需要 J2SE 1.5(也被称作 Java 5)或者更高版本的支持。Apache 的 OpenJPA 则是当前诸多热门 Java 持久化实现之一。Apache OpenJPA 是 Apache 软件基金下面的一个 Java 持久化技术,它可以被当作一个单独的 POJO(Plain Old Java Object)持续化使用,或者被集成到任意 Java EE 兼容的容器或其它的轻量级框架(例如 Tomcat、Spring)等一起使用。
![]() ![]() |
![]()
|
标准 JPA annotation 及其在 OpenJPA 中的应用
标准 JPA 提供了不少 annotation,在 OpenJPA 中这些标准的 annotation 的用法如下:
@entity
@entity 用来声明一个类为一个持久化的实体类。该 annotation 只有唯一可选的属性,即 name 用于指定实体的名称。
@table
@table 描述了一个实体类对应的数据库表的信息。@table 有以下属性:
- String name: 表的名称。
- String schema: 该表对应的 schema,如果没有指定 schema 的名称,JPA 在数据库连接时使用默认的 schema。
- String catalog: 表的目录。如果没有指定目录,JPA 在数据库连接时使用默认值。
- UniqueContraint[] uniqueContraints: 应用于表的唯一性限制条件。Uniquecontraint 本身是个 annotation, 用于保证数据库表的某一列(简单数据类型或者复杂数据类型)在每一个记录中都是唯一的。
@column
@column 定义了数据库表项的每一列的属性,具体内容包括:
- String name: 指定列的名称,默认值为类的 field 名称。
- String columnDefinition: 数据库系统特定的列类型名称。在 OpenJPA 中,该属性只适用于数据库系统通过那些能够从元数据直接创建表的数据库系统。在表的创建过程中,如果该属性值非空,那么数据库系统就以该属性指定的类型来定义指定的列。
- int length:指定列的长度。
- int precision:小数列的精度。
- int scale:定义小数类型的列最多可以容纳的数字数目
- boolean nullable: 该列是否能够接受非空的记录。
- boolean insertable:定义该列是否可以通过 SQL INSERT 语句中指定生成,默认值为 true。
- boolean updatable:定义该列是否可以通过 SQL UPDATE 语句中生成,默认值为 true。
- String table:有的时候列并非定义在主表下,table 属性就提供了将列映射到次表下的方法。
@ Inheritance
在面向对象的类之间存在继承关系,@inheritance 的出现就将继承关系能够映射到关系数据库模型中。在实体基类中定义继承策略
InheritanceType stragegy:用以声明继承策略的枚举类型,可取的值包括 SINGLE_TABLE,JOINED 和 TABLE_PER_CLASS:
- InheritanceType.SINGLE_TABLE:把所有的子类及其父类都映射到同一张表中。
- InheritanceType.JOINED: 类图中每个类都映射到各自的表项,但是每张表中仅仅包含在各个层次的类中定义的列。
- Inheritance.TABLE_PER_CLASS:类图中每个类都映射到各自的表现,和 JOINED 不同之处在于每张表包含了对应的实体类所有的列。
清单 1. JPA annotation 使用范例
@Entity(name="OperatingSystem") @Table(name="OperatingSystem") @Category(value="System") @Inheritance(strategy=InheritanceType.JOINED) public class OperatingSystem extends System{ … /** * A string describing the operating system version number. */ @Column(name="OSVersion",length=64) @Basic private String OSVersion; … } |
![]() ![]() |
![]()
|
扩展的 annotation 及其使用
在实际应用中,JPA 已有的 annotation 往往不能满足 OR 映射的所有需求。以下介绍通过自定义 Annotation 来扩充 OpenJPA 功能的方法,从而可以更好满足实际应用的需求。
@AbstractWithTable
在 OpenJPA 中,针对实体之间的继承关系如何在数据库中展现,共有三种方式:
SINGLE_TABLE,TABLE_PER_CLASS 和 JOINED。
当应用程序使用 TABLE_PER_CLASS 这种方式时,Java 的抽象类(Abstract Class)在 OpenJPA 中不会被映射到数据库中的表项,同时抽象性阻止了用户不能通过 OpenJPA 直接对该类进行持久化的操作。有些应用需要保持一个类在持久化操作方面的抽象性,但是又能保证该类在数据库中有对应的表项。那么该类必须被定义为 Java 的具体类,同时引入 @AbstractWithTable(如清单 2):
清单 2. @AbstractWithTable 的定义
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface AbstractWithTable { } |
@AbstractWithTable 的使用方式如清单 3 所示:
清单 3. 应用 @AbstractWithTable 定义实体类
@AbstractWithTable @Entity @EntityListeners(value = { TestAWTListener.class }) public class TestAWT { @Basic public String p; } |
从清单 3 中可见,除了 @Entity 之外,@AbstractWithTable 和 @EntityListeners 也被应用到类 TestAWT 中。其中 @AbstarctWithTable 就是用来指定这个抽象类需要一个数据库表,同时类 TestAWT 并没有使用 abstract 这个 Java 关键字。
OpenJPA 提供了 @EntityListeners 指定进行持久化操作前后的回调函数。定义 TestAWT 的回调函数如下清单 4 所示。
清单 4. 处理 @AbstractWithTable 的回调函数
public class TestAWTListener { @PrePersist public void prePersist(Object obj) throws Exception{ AbstractWithTable awt = obj.getClass().getAnnotation(AbstractWithTable.class); if (awt != null) { System.out.println("abstract class cannot be persisted"); throw new Exception(); } } } |
在回调函数中检测相关对象是否设置了 @AbstractWithTable,如果 @AbstractWithTable 被应用到某个类,异常将被抛出来从而阻止将此对象存入数据库中。这样通过 @AbstractWithTable 和回调函数就达到了控制类的 JPA 的持久化操作权限和该类能映射到数据库的表项的目的。
@MaximumValue 和 @MinimumValue
数据库中最常见的情况就是对某个字段的取值范围定义约束条件(Constraint),比如学生的考试成绩取值范围必须在 0 到 100 这个范围内。但是 OpenJPA 没有提供相应的 Annotation 来指定某个属性的取值范围。
为了解决这个问题,自定义的 Annotation:@Maximum 和 @Minimum 被引入,如下清单 5 和 6 所示。它们分别用来表示最大值和最小值。
清单 5. @MaximiumValue 和 @MinimiumValue 的定义
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface MaximumValue { String value(); } @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface MinimumValue { String value(); } |
清单 6. 应用 @Maximium 和 @Minimium
@Entity @EntityListeners(value = { TestMaxMinListener.class }) public class TestMaxMin { @Basic @MinimumValue("1") @MaximumValue("100") public Integer value; } |
@MaximiumValue 和 @MinimiumValue 被应用于 Java 类的域中。
与 3.1 类似,在这里设置了回调函数所在的类 TestMaxMinListener,函数如清单 7 所示:
清单 7. 处理 @ MaximiumValue 和 @MinimiumValue 的回调函数
public class TestMaxMinListener { @PrePersist public void prePersist(Object obj) throws Exception { TestMaxMin tmm = (TestMaxMin)obj; Field value = tmm.getClass().getDeclaredField("value"); MaximumValue max = value.getAnnotation(MaximumValue.class); MinimumValue min = value.getAnnotation(MinimumValue.class); if (tmm.value > max.value() || tmm.value < min.value()){ System.out.println("The value property is invalid."); throw new Exception(); } } } |
这个 TestMaxMinListener 的作用就是检测相应的属性值是否在规定的范围内,如果超出了规定的范围,那么就会抛出异常,从而防止将错误的数据存入数据库中。
@Unsigned
与上面的情况相同,对于某些属性的值,我们希望它是一个无符号整形数。而在 OpenJPA 中也没有相关的 Annotation 来指定。可以自定义一个 Annottation:@Unsigned 用来表示某个属性的值是无符号数。
清单 8. @Unsigned 的定义
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface Unsigned { } |
在 Entity Class 当中可以直接把 @Unsigned 设置在相应的属性前面 , 如清单 9 所示:
清单 9. 应用 @Unsigned
@Unsigned @Basic private Integer value; |
@Unsigned 的处理方式和 @MaximiumnValue 类似,这里就不再一一赘述。
@Required
数据库对表中的各项字段可以定义限制条件,如某些字段不能为空,某些字段为主键等。OpenJPA 定义的 @Column 中提供了字段以限制条件的映射,参考清单 11 中 @Column,将 nullable 设置为 false 来控制字段不能为空。
但是在图 1 所示的应用中,OnlineGame.ServiceProvider 不能为 null,同时必须 SINGLE_TABLE 的继承策略(参照表 1)来映射这一组类,如果仅仅应用 @Column 的 nullable 属性,我们将不得不面临这样一个问题:当试图持久化一个 PCGame 的实例到数据库中时,数据库系统将会提示该数据不能插入,因为数据系统对插入表 1 中的数据要求 ServiceProvider 不能为空。
图 1. 类图
![图 1. 类图](http://pub.chinaunix.net/uploadfile/201001/20100130030820477.gif)
表 1 . 用 SINGLE_TABLE 在 DB 中存储 Game,OnlineGame 和 PCGame
Game 的属性 | OnlineGame 的属性 | PCGame 的属性 | |||
Name | Version | Type | ServiceProvider | … | … |
难道为了应用 SINGLE_TABLE 就不得不丢弃字段的约束条件么? @Required 就应运而生了,定义如清单 10。@Required 被用来标记 p1,p2 不能为 null。
就需要在我们自己的程序里面检测 @Required,并对相应的属性值进行判断。这一步通常可以放在 EntityListener 中的 PrePersist 这个回调(Callback)函数中进行检测。
清单 10. @Required 的定义
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface Required { } |
在 OnlineGame Class 中,可以通过如下清单 11 所示方式设置属性 ServiceProvider:
清单 11. 应用 @Required
@Column(name="",nullable=true) @Basic @Required private String ServiceProvider; |
数据库不再对 ServiceProvider 是否为空做出约束,但是通过实现标记为 @prePresist 的回调函数,函数中检测是否有 @Required,如果有 @Required,该字段就不能为空。如此 ServiceProvider 的约束条件就不会再成为约束类 PCGame 的条件。
@RelationshipMetaData
在 OpenJPA 中,有四个 Annotation 可以用来标识一个实体之间的关系,它们分别是:@OneToOne,@OneToMany,@ManyToOne 和 @ManyToMany。其中每一个 Annotation 都有以下几个属性:targetEntity,mappedBy,cascade,fetch 和 optional。
图 2. 实体 Employee 和 SoftwareLicense 的 ER 图
![图 2. 实体 Employee 和 SoftwareLicense 的 ER 图](http://pub.chinaunix.net/uploadfile/201001/20100130030820754.gif)
尽管包括了一对一、一对多和多对多的情况,但是为了将图 2 所示 ER 关系图映射到关系数据库,以上的 Annotation 就不能满足要求。图 2 的关系 Owns 是一个多对多的关系,Owns 包括了两个属性 RequestDate 和 ExpiredDate,Owns 必须对应到一个实体类。类 Employee、SoftwareLicense 和 Owns 之间分别存在着多对一的关系,@ManytoOne 或者 @OnetoMany 虽然能够标识出关系,但是并不能标识出关系的源或者目的是一个实体还是关系,@RelationshipMetaData 就有了用武之地,其定义如清单 12 所示:
清单 12. @RelationshipMetaData 的定义
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface RelationshipMetaData { boolean isSource(); boolean isManyToManyWithProperties(); } |
有了 @RelationshipMetaData,图 2 对应的实体就可以被定义为:
清单 13. 应用 @RelationshipMetaData 实现图 2 的 OR 映射
@Table(name="Employee") @Entity(name="Employee") public class Employee { … @RelationshipMetaData(isSource=true,isManyToManyWithProperties=true) @OneToMany(mappedBy=”Source”) private java.util.Collection<Employee_owns_SoftwareLicense> owns_SoftwareLicense; … }; @Table(name="SoftwareLicense") @Entity(name="SoftwareLicense") public class SoftwareLicense{ … @RelationshipMetaData(isSource=false,isManyToManyWithProperties=true) @OneToMany(mappedBy=”Target”) private java.util.Collection<Employee_owns_SoftwareLicense> Employee_owns; … } @Table(name=" Employee_owns_SoftwareLicense ") @Entity(name=" Employee_owns_SoftwareLicense ") public class Employee_owns_SoftwareLicense{ … @ManyToOne private Employee Source; @ManyToOne private SoftwareLicense Target; … } |
![]() ![]() |
![]()
|
总结
本文首先介绍了 JPA 和 Apache OpenJPA,以及它们在将面向对象的实体映射为关系数据库表项 (OR Mapping) 方面的应用。然后介绍了 JPA 提供的标准 Annotations, 并且举例介绍了其用途。但实践表明已有的 annotation 常常不能满足需求,结合笔者的经验,一些扩展的常用的 annotation 以更好的满足 OR Mapping 的需求。通过提供自定义的 annotation,不仅弥补了目前 OpenJPA 提供的标准 annotation 的功能缺陷,而且提升了用户对 OR Mapping 过程的自适应调节和更灵活的控制,对于满足有特殊 OR Mapping 需求的开发具有非常实用的价值。