如何扩展 JPA Annotation 以更好的支持 OR Mapping

小编辑 发布于 2010/01/29 20:26
阅读 632
收藏 3

随着面向对象的数据模型被广泛应用,将面向对象的实体映射为关系数据库表项(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. 类图

表 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 图

尽管包括了一对一、一对多和多对多的情况,但是为了将图 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 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;

}

@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 需求的开发具有非常实用的价值。

加载中
返回顶部
顶部