扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
尽管有不少选择可以用来来构建应用程序的持久化层,但是并没有一个统一的标准可以用在Java EE环境和Java SE环境中。EJB3Java持久化API为我们带来了好消息,作为EJB 3.0规范(JSR-220)中的一部分,它标准化了Java平台下的持久化API。JSR-220已经被O-R Mapping软件生产商广泛支持,例如TopLink 和 Hibernate,同时它还被一些应用服务器生产商和JDO生产商所支持。EJB3规范为Java企业应用构建持久化层提供了一个强制性的选择。
在这篇文章中,笔者将会使用一个简单的对象模型作为例子来介绍EJB3 Java 持久化 API。
领域模型
当你构建一个企业应用时,你首先设计需要持久化到数据库中的领域模型。然后,你需要和数据库设计人员一起设计好数据库结构。领域模型是持久化对象或者实体的代表。一个实体可以是一个人,一个地方,或者任何其他你想要存储的数据。它同时包含了数据和行为。一个rich领域模型具有所有的OO的特征,例如继承和多态(inheritance and polymorphism)。
我们作为示例使用的这个简单的领域模型如下,部门(Department)和雇员(Employee)实体之间具有双向的一对多关系,而全职员工(FullTime)和承包工(Contractor)实体都是从雇员实体继承而来。
图 1. 示例领域对象模型
O-R映射框架和EJB3 JPA的基础
如果你使用过Oracle TopLink这样的O-R映射框架构建过应用程序的持久化层,那么你会注意到每个持久化框架都会提供3种机制
1. 声明性的O-R 映射方法。这种方法,叫做O-R映射元数据,使得你可以将一个对象映射到数据库中的一个或者多个表。大部分的O-R映射框架都使用XML来存储O-R映射的元数据。
2. 用来操作实体的API(例如,来执行CRUD操作)。API让你用来持久化,获取,更新或者删除对象。基于O-R映射元数据和API的使用,O-R映射框架代替你来完成各种数据库操作。API将你从繁琐的JDBC和SQL代码中解救出来。
3. 一种查询语言来获取对象。这是持久化操作的很重要的一个方面,因为不合适的SQL语句会使得你的数据库操作变慢。这种查询语言避免了在应用程序中混杂大量SQL语句的现象。
EJB3 Java 持久化API标准化了Java平台下的持久化操作,它提供一种标准的O-R映射机制,一组EntityManager API来进行CRUD操作,以及一种扩展的EJB-QL语言来获取实体。笔者将在分别讨论这3个方面。
元数据标注
Java SE 5.0 引入了元数据标注。Java EE的所有组件,包括 EJB3 JPA,都大量使用了元数据标注来简化企业Java应用的开发。想要了解更多的元数据标注方面的知识,请参阅Kyle Downey 写的Bridging the Gap: J2SE 5.0 Annotations文章。在EJB3 JPA中,元数据可以用来定义对象,关系,O-R映射以及持久化上下文(Context)的注入。JPA同样提供了使用XML描述符来提供持久化元数据的方法。而笔者则会重点讲述使用标注的方式,因为这使得开发变得更加简单。但是,在产品部署阶段,你或许会倾向于使用XML描述符,你可以使用XML描述符来覆盖标注定义的持久化行为。
在JPA中将O-R映射标准化
定义持久化对象:实体
一个实体是一个轻量级的领域对象――一个需要持久化到关系数据库中的POJO。与其他的POJO一样,一个实体可以是抽象类或者具体类,而且它可以从其他POJO扩展得到。可以使用javax.persistence.Entity标注将一个POJO标注成一个实体
下面是如何将领域模型中的Department对象变成一个实体的例子:
package onjava; import java.io.Serializable; import java.util.Collection; import javax.persistence.*; @Entity @NamedQuery(name="findAllDepartment", query="select o from Department o") @Table(name="DEPT") public class Department implements Serializable { @Id @Column(nullable=false) protected Long deptNo; @Column(name="DNAME") protected String name; @Column(name="LOC") protected String location; @OneToMany(mappedBy="department") protected Collection<Employee> employees; public Department() { } ... public Collection<Employee> getEmployees() { return employees; } public void setEmployees(Collection<Employee> employees) { this.employees = employees; } public Employee addEmployee(Employee employee) { getEmployees().add(employee); employee.setDepartment(this); return employee; } public Employee removeEmployee(Employee employee) { getEmployees().remove(employee); employee.setDepartment(null); return employee; } } |
每一个实体都有一个主键;可以在一个持久化字段(field)或者属性上使用Id标注来将它作为一个主键。一个实体可以通过使用字段或者属性(通过 setter and getter 方法)来维护自己的状态。这取决于你在何处使用Id标注。上面的例子中采用了基于字段的访问,我们在deptNo字段上使用了Id标注。如果要使用基于属性的访问,你需要在属性上使用Id这样的标注。
@Id public Long getDeptNo() { return deptNo; } public void setDeptNo(Long deptNo) { this.deptNo = deptNo; } |
必须注意,在一个实体继承体系中的所有实体,都必须使用通用的访问类型,或者都使用字段,或者都使用属性。
一个实体中定义的所有字段,默认情况下,都会进行持久化。如果你不想存储某个字段(或者属性),你必须将字段(或者属性)定义成临时的,通过采用@Transient标注或者采用transient修饰符。
嵌入对象
一个嵌入对象是一个自己不具有id的持久化对象。它是另外一个实体的一部分。例如,我们可以假定Address对象没有自己的id,并且它作为Employee实体的一部分进行存储。因此,Address是一个嵌入对象。
你可以采用如下的方法来创建一个嵌入对象
@Embeddable public class Address { protected String streetAddr1; protected String streetAddr2; protected String city; protected String state; .. } |
下面是如何将一个对象嵌入到一个目标对象中
@Entity public class Employee { @Id @GeneratedValue(strategy=GenerationType.AUTO) protected Long id; ... @Embedded protected Address address; ... } |
关系
在一个典型的领域模型中,实体可能互相联系或者存在一定的关系。两个实体之间的关系可能是一对一(one-to-one),一对多(one-to-many),多对一(many-to-one),多对多(many-to-many)。实体之间的这些关系可以分别使用OneToOne, OneToMany, ManyToOne, or ManyToMany标注来描述。我们的示例在Department和Employee实体上采用了双向的OneToMany关系。
既然我们在实体中使用了基于字段的访问,那么我们就在Department实体的关系字段上使用标注,如下:
@OneToMany(mappedBy="department") protected Collection<Employee> employees ; |
对于一个双向的关系来说,你必须指定mappedBy元素,就像上面那样,通过指明拥有这个关系的字段名或者属性名来指出反向的关系如何进行映射。
标准化O-R 映射
你可以使用Java标注或者XML来进行实体的O-R映射的定义。EJB3 JPA定义了几个标注来进行O-R映射,例如Table,SecondaryTable, Column, JoinColumn, 以及 PrimaryKeyJoinColumn等标注。参考EJB3 JPA规范来获取所有标注的信息。
在我们的例子中,你可以使用Table标注来定义该实体应该映射到那个表中,例如:
@Table(name="DEPT") public class Department implements Serializable { |
EJB3 JPA中,各种映射都普遍具有默认值。如果你不定义表映射的话,持久化提供者会假定这个实体将要映射到和实体类类名同名的表中(在这个例子中就是Deparment表)。如果你的实体需要映射到多个表中,你可以使用SecondaryTable标注。
你可以使用Column标注将一个字段或者属性映射到数据库中的一个字段,就像下面这样:
@Column(name="DNAME") protected String name; |
这里,DNAME是需要持久化的字段name要映射的数据库中的字段名。如果你不使用Column定义字段的映射的话,持久化引擎会尝试使用与字段名或者属性名相同的数据库字段名来进行持久化。
实体继承
EJB3 JPA 采用多种方法来支持实体继承。它需要两种类型的继承表映射策略:Single-table-per-entity 继承层次策略和Joined-Subclass策略。最好避免使用可选的table-per-class层次。
single-table-per-entity 继承层次策略允许一个继承层次中的所有实体都映射到单一的表中。在我们的示例中,FullTime和Contractor都继承了Employee,所有它们的示例都会被映射到一个叫做EMP的表中。换句话说,所有与Employee,FullTime和Contractor相关的数据都会被存储到同一个表中。
如果你使用Joined-Subclass策略,你可以将通用的数据映射到超类(例如Employee)表中,同时为每个子类定义一个表来存储子类特有的数据。
必须在超类上使用Inheritance标注来指明采用的继承映射策略,就像下面的代码那样。这个例子演示了实体继承层次使用single-table-per-entity策略的方法。
@Entity @Table(name="EMP") @Inheritance(strategy=InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name="EMPLOYEE_TYPE", discriminatorType=DiscriminatorType.STRING, length=1) public abstract class Employee implements Serializable { ... } |
每个子类必须指明用于区分实体类型的值,如下所示:
@Entity @DiscriminatorValue(value="F") public class FullTime extends Employee { @Column(name="SAL") protected Double salary; @Column(name="COMM") protected Double commission; @Column(name="DESIG") protected String designation; ... } |
Entity Manager API:实体操作的标准API
javax.persistence.EntityManager 用来管理实体的声明周期,它提供了几个方法来进行实体的CRUD操作。
EntityManager API 在一个事务上下文(transaction context)中被调用。你可以从EJB容器以外来调用它,例如,你可以从一个Web应用中来调用,使用EntityManager API并不一定需要一个sesssion bean门面(facade).
在你进行实体操作之前,你必须获取一个EntityManager的实例。你可以使用容器管理的实体管理器,也可以使用应用程序管理的实体管理器,同时你可以使用JNDI查询或者依赖注射来获取EntityManager得实例。顾名思义,在容器管理的Entity Manager的情况下,Java EE容器管理Entity Manager的生命周期。这通常在企业Java应用中使用。
你可以使用PersistenceContext标注来进行依赖注射而获取一个容器管理的entity manager的实例,如下:
@PersistenceContext(unitName="onjava") private EntityManager em; |
如果你想要使用应用程序管理的entity manager,你必须自己管理它的声明周期。你可以使用如下方法创建它的一个实例。
@PersistenceUnit(unitName="onjava") private EntityManagerFactory emf; private EntityManager em = emf.createEntityManager(); |
然后,就可以使用EntityManager实例来进行实体的CRUD操作了。为了关闭应用程序管理的entity manager的实例,在进行完操作以后调用em.close()方法。
就像前面提到的,一个包含了任何数据库改变的entity manager的操作都必须在一个事务上下文中进行。
下面的表列出了EntityManager接口的一些重要方法,这些方法用于进行实体操作。
方法 | 目的 |
public void persist(Object entity); | 持久化一个实体的实例 |
public |
合并一个detached实体 |
public void remove(Object entity); | 删除实体的实例 |
public |
通过主键获取实体的实例 |
public void flush(); | 将实体的状态与数据库同步 |
可以使用persist()方法来持久化一个实体的实例。例如,如果想要持久化Contractor的一个实例,使用以下的代码:
@PersistenceContext(unitName="onjava") private EntityManager em; ... Contractor pte = new Contractor(); pte.setName("Nistha") pte.setHourlyRate(new Double(100.0)); em.persist(pte); |
如果持久化了一个实体,并且实体上的关系的CascadeType被设置为PERSIST或者是ALL的话,那么任何和该实体关联的实体的状态变化都会被持久化。除非你使用一个扩展的持久化上下文,否则的话,实体在事务结束以后就会变成detached状态。merge操作允许你使用一个持久化上下文将一个detached实体存储到数据库中,这个detached实体的状态将会和数据库同步。这可以使你摆脱在EJB 2.x 时代普遍使用的DTO这种反模式,因为现在实体都是POJO,可以在各个层之间传递。对于实体类的唯一要求就是要实现java.io.Serializable接口。
查询API
持久化的另一个重要问题就是实体的获取。当使用EJB3 JPA时,查询使用java 持久化查询语言来表达。JPQL是EJB QL(EJB2.0中引入)的一个扩展。但是,EJB3 JPA,解决了EJB QL的一些限制并且增加了新的功能,使之成为一个强大的查询语言。
JPQL对于EJBQL 2.x的增强
以下是JPQL的一些新功能:
而且,你可以使用native SQL来查询实体如果你需要特定的数据库查询扩展的话。
动态查询 vs. 命名查询
你可以使用动态查询或者命名查询。一个命名查询和实体存储在一起,并且可以在程序中重复使用。
为了创建一个动态查询,使用entity manager接口的createQuery方法,如下:
Query query = em.createQuery( "select e from Employee e where e.empNo > ?1"); query.setParameter(1,100); return query.getResultList(); |
如果想要使用命名查询来进行这个查询,在实体类中使用NamedQuery标注,如下:
@Entity @NamedQuery(name="findAllEmployee", query="select e from Employee e where e.empNo > ?1") public abstract class Employee implements Serializable { } |
为了执行一个命名查询,首先使用EntityManager接口的createNamedQuery方法来创建一个Query实例,如下:
query = em.createNamedQuery(" findAllEmployee"); query.setParameter(1,100); return query.getResultList(); |
命名参数
你可以在EJBQL中使用命名参数来代替位置参数,例如,你可以使用如下方法来改写上面的查询:
"select e from Employee e where e.empNo > :empNo " |
如果在查询中使用命名参数,你必须使用如下的方法来设置参数:
query = em.createNamedQuery("findAllEmployee"); query.setParameter("empNo",100); return query.getResultList(); |
打包
EJB3 JPA标准化了POJO的持久化操作。因此,实体并不限于EJB模块,它们可以打包在一个Web模块中,一个ejb-jar模块,EAR级别的类模块,或者一个标准的Jar文件中。你也可以在J2SE环境中使用实体。你必须将一个部署描述符文件(在persistence.xml)打包进去。
<persistence> <persistence-unit name="onjava"> <provider>oracle.toplink.essentials.PersistenceProvider</provider> <jta-data-source>jdbc/OracleDS</jta-data-source> ... </persistence-unit> </persistence> |
这个部署描述符确定了持久化提供者,持久化单元以及持久化单元使用的数据源。顾名思义,一个持久化单元就是需要一起管理的一组实体。如果你在一个特定的模块中定义了唯一一个持久化单元,你就不需要在persistence.xml中定义实体类,持久化提供者会自动找到实体类。
TopLink Essentials:参考实现
TopLink Essentials,是从主要的一个商业O-R映射框架Oracle TopLink中衍生出来的,这是EJB3 JPA的一个参考实现。可以从Java Persistence API implementation home page找到它。
你可以在一个该参考实现的服务器或者其他的遵循EJB3 JPA固定的应用服务器上使用本文中的代码。
EJB3 JPA工具
开发工具确实能够帮助你创建更好的应用程序——并且如果你使用XML来定义O-R映射,操作会很繁琐。Eclipse Dali O-R映射项目致力于使得EJB3 JPA变得更加简单,它在Eclipse Web工具项目中提供了综合的工具,这个项目是Oracle领导的,并且被JBoss,BEA以及Versant所支持。想要了解更多关于Dali的信息请访问它的主页。
同样的,Oracle JDeveloper 10.1.3和BEA Workshop studio这样的工具也支持EJB3 JPA。
结论
EJB3 Java持久化API标准化了Java平台的持久化操作。它通过使用元数据标注简化了透明持久化操作。几个应用服务器,包括Oracle Application Server 10g (10.1.3), Sun公司的开源的 GlassFish Application Server, and JBoss Application Server 4.0, 都提供了EJB3 规范的支持。当Java EE5.0和EJB 3.0最终确定下来,你可以很快看到很多领先的应用服务器和持久化提供者都实现了EJB3 Java Persistence API。你可以使用GlassFish项目提供的参考实现来开始EJB3的持久化的使用。
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者