原文摘自IBM开发者论坛:http://www.ibm.com/developerworks/cn/java/j-lo-jpa-anntation/index.html
随着面向对象的数据模型被广泛应用,将面向对象的实体映射为关系数据库表项(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 用来声明一个类为一个持久化的实体类。该 annotation 只有唯一可选的属性,即 name 用于指定实体的名称。
@table 描述了一个实体类对应的数据库表的信息。@table 有以下属性:
@column 定义了数据库表项的每一列的属性,具体内容包括:
在面向对象的类之间存在继承关系,@inheritance 的出现就将继承关系能够映射到关系数据库模型中。在实体基类中定义继承策略
InheritanceType stragegy:用以声明继承策略的枚举类型,可取的值包括 SINGLE_TABLE,JOINED 和 TABLE_PER_CLASS:
@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; … }
在实际应用中,JPA 已有的 annotation 往往不能满足 OR 映射的所有需求。以下介绍通过自定义 Annotation 来扩充 OpenJPA 功能的方法,从而可以更好满足实际应用的需求。
在 OpenJPA 中,针对实体之间的继承关系如何在数据库中展现,共有三种方式:
SINGLE_TABLE,TABLE_PER_CLASS 和 JOINED。
当应用程序使用 TABLE_PER_CLASS 这种方式时,Java 的抽象类(Abstract Class)在 OpenJPA 中不会被映射到数据库中的表项,同时抽象性阻止了用户不能通过 OpenJPA 直接对该类进行持久化的操作。有些应用需要保持一个类在持久化操作方面的抽象性,但是又能保证该类在数据库中有对应的表项。那么该类必须被定义为 Java 的具体类,同时引入 @AbstractWithTable(如清单 2):
@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 的持久化操作权限和该类能映射到数据库的表项的目的。
数据库中最常见的情况就是对某个字段的取值范围定义约束条件(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 的作用就是检测相应的属性值是否在规定的范围内,如果超出了规定的范围,那么就会抛出异常,从而防止将错误的数据存入数据库中。
与上面的情况相同,对于某些属性的值,我们希望它是一个无符号整形数。而在 OpenJPA 中也没有相关的 Annotation 来指定。可以自定义一个 Annottation:@Unsigned 用来表示某个属性的值是无符号数。
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface Unsigned { }
在 Entity Class 当中可以直接把 @Unsigned 设置在相应的属性前面 , 如清单 9 所示:
@Unsigned @Basic private Integer value;
@Unsigned 的处理方式和 @MaximiumnValue 类似,这里就不再一一赘述。
数据库对表中的各项字段可以定义限制条件,如某些字段不能为空,某些字段为主键等。OpenJPA 定义的 @Column 中提供了字段以限制条件的映射,参考清单 11 中 @Column,将 nullable 设置为 false 来控制字段不能为空。
但是在图 1 所示的应用中,OnlineGame.ServiceProvider 不能为 null,同时必须 SINGLE_TABLE 的继承策略(参照表 1)来映射这一组类,如果仅仅应用 @Column 的 nullable 属性,我们将不得不面临这样一个问题:当试图持久化一个 PCGame 的实例到数据库中时,数据库系统将会提示该数据不能插入,因为数据系统对插入表 1 中的数据要求 ServiceProvider 不能为空。
表 1 . 用 SINGLE_TABLE 在 DB 中存储 Game,OnlineGame 和 PCGame
Game 的属性 | OnlineGame 的属性 | PCGame 的属性 | |||||
|
|
... |
难道为了应用 SINGLE_TABLE 就不得不丢弃字段的约束条件么? @Required 就应运而生了,定义如清单 10。@Required 被用来标记 p1,p2 不能为 null。
就需要在我们自己的程序里面检测 @Required,并对相应的属性值进行判断。这一步通常可以放在 EntityListener 中的 PrePersist 这个回调(Callback)函数中进行检测。
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface Required { }
在 OnlineGame Class 中,可以通过如下清单 11 所示方式设置属性 ServiceProvider:
@Column(name="",nullable=true) @Basic @Required private String ServiceProvider;
数据库不再对 ServiceProvider 是否为空做出约束,但是通过实现标记为 @prePresist 的回调函数,函数中检测是否有 @Required,如果有 @Required,该字段就不能为空。如此 ServiceProvider 的约束条件就不会再成为约束类 PCGame 的条件。
在 OpenJPA 中,有四个 Annotation 可以用来标识一个实体之间的关系,它们分别是:@OneToOne,@OneToMany,@ManyToOne 和 @ManyToMany。其中每一个 Annotation 都有以下几个属性:targetEntity,mappedBy,cascade,fetch 和 optional。
图 2. 实体 Employee 和 SoftwareLicense 的 ER 图
尽管包括了一对一、一对多和多对多的情况,但是为了将图 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 需求的开发具有非常实用的价值。