腾讯游戏客户端平台:理解JPA,第一部分:面向对象的数据持久化方案

来源:百度文库 编辑:九乡新闻网 时间:2024/05/01 03:01:07
很多JAVA项目在处理数据持久化的时候,都努力想寻找一种很自然的面向对象的方式。JPA,作为JSR220的一个产物,提供了一种标准化的操作方式。这个介绍JPA的系列文章一共有2部分,在第一部分里,您将了解到JPA是如何使得数据持久化操作融入到你的面向对象架构当中的。
Why JPA?      对于许多JAVA开发者来说,都会问到同一个问题:“为什么要推出JPA,既然Hibernate和Toplink等技术已经非常成熟,我为什么还要学习JPA?”答案很简单,JPA并不是一项新技术,更确切地说,它综合了Hibernate、Toplink和JDO等各种数据持久化技术的精髓,从而产生一个标准的规范来处理数据持久层,这样就不依赖任何一个特定的产品提供商。

    不管你是否喜欢,数据都是任何一个应用程序中不可缺少的一部分,尤其是哪些面向对象的应用程序。JAVA程序员在处理数据持久层的时候,比较传统的方式是写一些复杂的SQL查询语句,但是随着应用程序规模的不断增长,这些内容会使得程序变得难以管理。如果能够用面向对象的方式来处理这些查询,充分运用“封装”、“抽象”、“继承”和“多态”等特性,这将是多么美妙的一件事情啊。

    事实上,JAVA社区已经开发出很多种面向对象的方式来处理数据持久化:EJB,JDO,Hibernate还有Toplink都是非常不错的解决这一问题的方案。而JPA,则是java EE 5规定的标准的持久化应用程序接口。JPA规范一开始是作为JSR 220:EJB 3.0规范的一部分,目的是简化EJB中实体bean编程模型。尽管它和Java EE 5.0中的实体bean相关,但是在容器之外,在java SE环境中,JPA也是可以使用的。

    在这篇文章中,你将会看到,借助于JPA中的标注,使用面向对象的方式处理数据持久化,是多么的简洁和优雅。这篇文章面向的读者是那些JPA的初学者,同时需要掌握一些基本的关系型数据库概念以及熟悉JAVA 5中的标注。JPA需要JAVA 5或者更高版本,因为它大量使用了JAVA中的新特性,比如标注和泛型。

OpenJPA以及样例程序

     在这篇文章中,我们将使用OpenJPA来进行演示,它是由Apache组织提供的一个JPA规范的具体实现。我之所以选择OpenJPA而不是其他供应商的产品,主要是因为它被集成在Weblogic、WebSphere和Geronimo等应用服务器当中。在我撰写本文的时候,OpenJPA的最新版本是1.0.1,可以通过Resources section这个链接来下载。如果你想使用其他的JPA实现,那么很明显你首先要读一读相关文档。

     在本文余下的部分当中,我将通过一个例子向您介绍JPA中的各种概念。这个例子是基于一个名叫XYZ的超市,既有网上店铺也有实体零售店。一开始,你将了解到如何使用JPA对客户模型进行CRUD操作。在后面的部分,你将了解到如何通过对象继承的方式来扩展CRUD操作。

     本文代码包包含了实体监听器以及文章中讨论的三种继承类型(单表、连接、每个类一个表)的代码。

JPA: 如何使用?

为了实现一个JPA兼容的程序,你需要如下三样东西:

  • 一个实体类
  • 一个 persistence.xml 文件
  • 一个功能类,用于完成插入、更新或者查找一个实体

     JPA只能用于处理数据持久化,下面让我们来看看如何通过JPA来设计数据的存储方式。假设你已经有一个 CUSTOMER 表, 如表1所示:

表 1. CUSTOMER 表的模式

NAME PK? TYPE NULL? CUST_ID Y INTEGER NOT NULL FIRST_NAME   VARCHAR(50) NOT NULL LAST_NAME   VARCHAR(50)   STREET   VARCHAR(50)   APPT   VARCHAR(20) NOT NULL CITY   VARCHAR(25)   ZIP_CODE   VARCHAR(10) NOT NULL CUST_TYPE   VARCHAR(10) NOT NULL LAST_UPDATED_TIME   TIMESTAMP NOT NULL

用于持久化的对象: 实体

     既然JPA是用于处理“实体-关系”映射的,那么接下来你就应该设计一个Customer实体对象。实体对象没有什么特别的,就是一个POJO类在加上一个 @Entity 标注,如清单1所示:

清单 1. Customer实体

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;

@Entity(name = "CUSTOMER") //Name of the entity
public class Customer implements Serializable{
private long custId;
private String firstName;
private String lastName;
private String street;
private String appt;
private String city;
private String zipCode;
private String custType;
private Date updatedTime;

// Getters and setters go here
......................
}

     Customer实体需要知道如何把他的属性映射到CUSTOMER表中。有两种方式可以做到,要么写一个名叫orm.xml的配置文件,要么如清单2所示,利用JPA的标注。

清单 2. 带有标注的 Customer 实体

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;

@Entity(name = "CUSTOMER") //Name of the entity
public class Customer implements Serializable{
@Id //signifies the primary key
@Column(name = "CUST_ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private long custId;

@Column(name = "FIRST_NAME", nullable = false,length = 50)
private String firstName;

@Column(name = "LAST_NAME", length = 50)
private String lastName;

// By default column name is same as attribute name
private String street;

@Column(name = "APPT",nullable = false)
private String appt;

// By default column name is same as attribute name
private String city;

@Column(name = "ZIP_CODE",nullable = false)
// Name of the corresponding database column
private String zipCode;

@Column(name = "CUST_TYPE", length = 10)
private String custType;

@Version
@Column(name = "LAST_UPDATED_TIME")
private Date updatedTime;

// Getters and setters go here
......................
}

让我们来仔细看看清单2中使用的标注。

  • 所有的标注都在 javax.persistence 中定义,所以你必须包含这个java包。
  • @Enitity 指明某一个类为实体类。如果实体的名字和表的名字不同,则要使用 @Table 标注;反之则不需要。
  • 如果属性的名字和表中相应列的列名不同,则需要使用 @Column 标注 (默认情况下,这两者的名字应该是相同的)
  • @Id 指明主键。
  • @Version 指明实体中的版本字段。JPA 使用版本字段来检测对于数据的并发修改。当JPA检测到有多个操作同时修改一个数据,它将向最后提交的事务抛出一个异常。这将保护你先前提交的事务能够稳定地提交数据。
  • 默认情况下,所有的字段都是 @Basic 类型的,将按原样保存到数据库中。
  • @GeneratedValue 指明一种策略来为ID字段分配一个唯一的值。可选的策略有 IDENTITY, SEQUENCE, TABLE, 和 AUTO。默认的策略是 auto,具体的实现有JPA提供商来完成。(OpenJPA 是用序列来实现的)

当你创建一个实体类的时候,如下几点需要牢记:

  • JPA 允许持久化类继承自非持久化类、持久化类继承自持久化类、非持久化类继承自持久化类。
  • 实体类必须有一个默认的无参数构造函数。
  • 实体类不能是 final 的。
  • 持久化类不能继承自某些特定的系统类,例如java.net.Socketjava.lang.Thread
  • 如果一个持久化类继承自一个非持久化类,那么父类中的属性是不能被持久化的。

持久化单元

     既然实体类已经完成,接下来就该处理 persistence.xml 配置文件了。 列表3所示的XML文件位于 META-INF 文件夹中; 它被用于指定持久化提供商的名字,实体类的名字,还有一些系统属性,例如数据库的URL、驱动、用户、密码等等。

列表 3. persistence.xml 文件样例





org.apache.openjpa.persistence.PersistenceProviderImpl

entity.Customer

value="jdbc:derby://localhost:1527/D:OpenJPADerbytestdb;create=true"/>
value="org.apache.derby.jdbc.ClientDriver"/>






关于 persistence.xml 文件有如下重要内容需要注意:

  • persistence.xml 可以包含多个持久化单元。每一个持久化单元都可以被不同的JPA提供商使用,或者能够被用来操作不同的数据库。
  • JPA提供商的名字在 标签中指定。OpenJPA 的提供商的名字是org.apache.openjpa.persistence.PersistenceProviderImpl.
  • 实体类的名字在 标签中指定。
  • 数据库连接属性可以在 标签中指定。注意各个提供商之间property name是不同的。
  • OpenJPA 拥有默认的日志能力,其默认级别是 INFO。

真正的展示

     既然准备工作已经完成,接下来我们就要写一个类,来向CUSTOMER表中插入一条记录,如清单4所示:

清单 4. 对象持久化的样例代码

public static void main(String[] args) {
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("testjpa");
EntityManager em = entityManagerFactory.createEntityManager();
EntityTransaction userTransaction = em.getTransaction();

userTransaction.begin();
Customer customer = new Customer();
customer.setFirstName("Charles");
customer.setLastName("Dickens");
customer.setCustType("RETAIL");
customer.setStreet("10 Downing Street");
customer.setAppt("1");
customer.setCity("NewYork");
customer.setZipCode("12345");
em.persist(customer);
userTransaction.commit();
em.close();
entityManagerFactory.close();
}

     让我们仔细研究一下清单4中的代码都是什么含义。首先出场的是 Persistence 类。javadoc 中说: "Persistence 类是一个自举类,用于获得 EntityManagerFactory类。" 就像这样:

EntityManagerFactory emf=Persistence.createEntityManagerFactory("testjpa");

Persistence 类的工作十分简单:

  • 在类路径中,它搜索META-INF/services/directory中存在的javax.persistence.spi.PersistenceProvider文件。它从每一个文件中读取PersistenceProvider实现类的名字。
  • 之后它利用persistenceUnitName为每一个 PersistenceProvider 调用 createEntityManagerFactory()函数,直到得到一个EntityManagerFactory。OpenJPA的提供商名字是org.apache.openjpa.persistence.PersistenceProviderImpl

     PersistenceProvider是如何得到正确的EntityManagerFactory呢?这个有提供商来实现。

    EntityManagerFactory 是一个工厂类用于产生 EntityManager. 在整个应用程序中,EntityManagerFactory 应该被缓存,并且针对每一个持久化单元,它只应调用一次。

    EntityManager用于管理实体,它负责进行添加、更新和删除。你无须一个事务就能找到一个实体,当然,如果你要进行添加、更新或删除操作的话,还是应该位于一个事务之中的。

     如果你在进行获取操作的时候没有位于一个事务当中,实体就不会处于受管状态。因此,每一次获取记录的操作,系统都会对数据库进行访问。在这种情况下,每一次操作都是独立的,系统访问一次数据库只会做一件事情,而不是积攒许多事情之后一起做。

     剩余的代码都很好懂,首先创建了一个 customer 对象,给相应的属性设置正确的值,然后将对象插入到数据库中,如清单5所示:

清单 5. 将对象持久化的代码片段

EntityTransaction userTransaction = em.getTransaction();
userTransaction.begin();
em.persist(customer);
userTransaction.commit();

     你可能已经注意到了,在我们的代码中,并没有明确的设置custId属性和updatedTime属性。因为主键的产生策略是AUTO,JPA提供商会小心地计算出主键。同样地,版本字段(在我们的例子中是updatedTime属性)也是由JPA提供商自动来计算生成的。

     现在你需要找到你刚才插入的记录。利用主键找到一个记录是非常简单的,如清单6所示:

清单 6. 将数据取出并组装成一个对象

....
OpenJPAEntityManager oem = OpenJPAPersistence.cast(em);
Object objId = oem.getObjectId(customer);
Customer cust = em.find(Customer.class, objId);
....


     由于我们无法预知主键的值,程序必须将 EntityManager 强制转化为 OpenJPAEntityManager,然后将刚刚存进数据库的customer对象传递给它,从而获得主键的值。对于这种操作,不同的提供商要写的代码可能不一样!!

一个复合主键

     现在我们来研究一下,如果一个实体的主键是由多个字段复合而成的,我们该如何通过主键来取得整条记录呢?

清单 7. 一个ID类

public class CustomerId {
public String firstName;
public String lastName;
// override equal() method
//override hascode() method
..................
}

     CustomerId 类可以是一个独立的类,也可以是一个内部类。如果它是一个内部类,那么它必须是static的,同时必须被实体类引用,就像清单8所展示的那样。很明显,在取得整条记录的时候,无论是复合主键还是单一主键,其操作都是一样的。

清单 8. 使用ID类

@Entity
@IdClass(Customer.CustomerId.class)
public class Customer implements Serializable{
@Id
@Column(name = "FIRST_NAME", nullable = false, length = 50)
private String firstName;

@Id
@Column(name = "LAST_NAME", length = 50)
private String lastName;

private String street;

@Column(name = "APPT",nullable = false)
private String appt;

................
}

回调函数

     为了便于在持久化操作的各个阶段处理一些事情,JPA 提供了回调函数。设想一下,你要更新一个客户的信息,而这个客户是本地人,所以你需要删除其zip码中的连字符,或者是在你取得一条记录之后,你要填写一些临时信息。在获取、插入和更新操作的前后,JPA 提供了监听器来帮你完成这些事情。对于回调函数,可以按照如下的方式进行标注:

  • @PostLoad
  • @PrePersist
  • @PostPersist
  • @PreUpdate
  • @PostUpdate
  • @PreRemove
  • @PostRemove

     你可以在实体类中直接写回调函数,也可以把回调函数写在一个单独的类中,然后让实体类通过 @EntityListeners 标注引用这个类,如清单9所示:

清单 9. 实现回调函数

@EntityListeners({CustListner.class})
@Entity(name = "CUSTOMER") //Name of the entity
public class Customer implements Serializable{
...
...
}
public class CustListner {
@PreUpdate
public void preUpdate(Customer cust) {
System.out.println("In pre update");
}
@PostUpdate
public void postUpdate(Customer cust) {
System.out.println("In post update");
}
}

内嵌对象

     正如你目前所看到的,在Customer实体中,地址信息是分为street、attp、city等几个字段位于其中。如果你想把地址信息提炼为一个单独的类,然后在Customer实体中引用这个类,该怎么做呢?毕竟,一个地址对象可以被用在许多类中,比如Customer、Employee、Order或者User等等。

     只要使用内嵌对象就能实现这个要求。你把地址信息提炼到一个单独的类中,然后将哪个类标注为“可内嵌的”,正如清单10所示,然后在Customer实体中通过 @Embedded 标注来引用这个地址类。

清单 10. 一个可内嵌的类

@Embeddable
public class Address implements Serializable{

private String street;

@Column(name = "APPT",nullable = false)
private String appt;

private String city;
..
..
}

     内嵌类和他的拥有者一同被映射为实体的某些状态。当然,它们不能被单独的查询。清单11就是一个使用了内嵌对象的实体。

Listing 11. A sample entity using an embedded object

@Entity
public class Customer {
...............
@Column(name = "FIRST_NAME", nullable = false,length = 50)
private String firstName;

@Embedded
private Address address;
..............
}

继承的力量

一个实体可以从如下几种方式中进行继承:

  • 另一个实体——无论它是具体的还是抽象的
  • 另一个非实体,它能够提供一些行为或非持久化状态。从这里继承而来的属性仍然是不可持久化的。
  • 映射过的超类,它能够提供一些公共的实体状态。数据库中不同的表可能会拥有相似的字段,但是这些表之间全没有任何关系,这时候就可以用这种继承方式。

     下面让我们来看看 JPA 中提供的不同的继承方式。就我们这个例子而言,假设有2中不同类型的客户:1、普通客户,他们只在实体零售店中购买物品;2、在线客户,她们通过Internet在网店中购买物品。

单表继承

     所谓单表继承,就是说整个继承架构中的所有实体都被存放在同一个表中。单表继承是默认策略。因此,对于清单12中的代码,你可以省略掉 @Inheritance这个标注,结果是完全一样的。

     在我们的例子程序中,无论是普通用户还是在线用户,都被存放在CUSTOMER表中,如表2所示:

表 2. 单表继承映射策略

ENTITY TABLE NAME Customer CUSTOMER OnlineCustomer CUSTOMER

     Customer 实体拥有 custId, firstName, lastName, custType, 和 address 等信息,对于 OnlineCustomer 实体,除了它所特有的 website 属性外,其余属性一律继承自 Customer 类。这个策略应该被反映在超类中,如清单12所示:

清单 12. 单表继承中的超类

@Entity(name = "CUSTOMER") 
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="CUST_TYPE", discriminatorType=DiscriminatorType.STRING,length=10)
@DiscriminatorValue("RETAIL")
public class Customer implements Serializable{
@Id
@Column(name = "CUST_ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private long custId;

@Column(name = "FIRST_NAME", nullable = false,length = 50)
private String firstName;

@Column(name = "LAST_NAME", length = 50)
private String lastName;

@Embedded
private Address address = new Address();

@Column(name = "CUST_TYPE", length = 10)
private String custType;
................
}

     就目前的代码而言,你可以暂时不理睬 DiscriminatorColumnDiscriminatorValue 标注,它们的功能我们在后面介绍。OnlineCustome 实体将是一个普通的实体,他继承自 Customer 类,如清单13所示:

清单 13. 单表继承中的子类

@Entity(name = "ONLINECUSTOMER") //Name of the entity
@DiscriminatorValue("ONLINE")
public class OnlineCustomer extends Customer{
@Column(name = "WEBSITE", length = 100)
private String website;
............
}

     现在你必须要创建一个 Customer 对象和 OnlineCustomer 对象,然后将它们持久化,如清单14所示:

清单 14. 在单表继承中持久化对象

......................
userTransaction.begin();
//inserting Customer
Customer customer = new Customer();
customer.setFirstName("Charles");
customer.setLastName("Dickens");
customer.setCustType("RETAIL");
customer.getAddress().setStreet("10 Downing Street");
customer.getAddress().setAppt("1");
customer.getAddress().setCity("NewYork");
customer.getAddress().setZipCode("12345");
em.persist(customer);
//Inserting Online customer
OnlineCustomer onlineCust = new OnlineCustomer();
onlineCust.setFirstName("Henry");
onlineCust.setLastName("Ho");
onlineCust.setCustType("ONLINE");
onlineCust.getAddress().setStreet("1 Mission Street");
onlineCust.getAddress().setAppt("111");
onlineCust.getAddress().setCity("NewYork");
onlineCust.getAddress().setZipCode("23456");
onlineCust.setWebsite("www.amazon.com");
em.persist(onlineCust);
userTransaction.commit();
......................

     执行了上述代码后,如果你去数据库中看一看 CUSTOMER 表,你会发现增加了2条记录。清单15所示的查询语句将返回数据库中的在线客户信息。

清单 15. 在单表继承中查询子类信息

..............
Query query = em.createQuery("SELECT customer FROM ONLINECUSTOMER customer");
List list= query.getResultList();
.................

     如果 CUSTOMER 表中既存储了Customer类,又存储了OnlineCustomer类,那么JPA是如何分辨它们的呢?JPA是如何只取得在线客户的信息的呢?实际上,如果你不给出一点点提示的话,JPA确实无法进行区分,这就是 @DiscriminatorColumn 标注的重要作用。它告诉 CUSTOMER 表那一个字段是用来区分 CUSTOMER 和 ONLINE CUSTOMER 的。@DiscriminatorValue 指出什么样的值来区分 CUSTOMER  和 ONLINE CUSTOMER。 @DiscriminatorValue 标注需要同时在超类和子类中标明。

     当你需要查询在线客户的时候,JPA 默默的按照清单16所展示的语句进行查询。

清单 16. 区分单表中的不同对象

SELECT t0.CUST_ID, t0.CUST_TYPE, t0.LAST_UPDATED_TIME, t0.APPT, t0.city, t0.street, t0.ZIP_CODE, 
t0.FIRST_NAME, t0.LAST_NAME, t0.WEBSITE FROM CUSTOMER t0 WHERE t0.CUST_TYPE = 'ONLINE'

连接表继承

     在连接表继承策略中,公共状态被存放在一个表中,子类特有的状态被存放在另一个表中,这两个表按照某种关系进行连接,如表3所示:

表 3. 连接表继承映射策略

ENTITY TABLE NAME Customer CUSTOMER OnlineCustomer ONLINECUSTOMER (only Website information is stored here; the rest of the information is stored in the CUSTOMER table)

     OnlineCustomerCustomer 的公共信息被存放在 CUSTOMER 表中, OnlineCustome 所特有的信息被存放在 ONLINECUSTOMER 表中,这两个表利用外键约束进行连接。从JPA实现的立场来看,在 OnlineCustomer 实体中唯一需要做出的调整是应该提供一个 JOINED策略,如清单17所示:

清单 17. 连接表继承中的超类

@Entity(name = "CUSTOMER") //Name of the entity
@Inheritance(strategy=InheritanceType.JOINED)
@DiscriminatorColumn(name="CUST_TYPE", discriminatorType=DiscriminatorType.STRING,length=10)
@DiscriminatorValue("RETAIL")

public class Customer implements Serializable{
@Id //signifies the primary key
@Column(name = "CUST_ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private long custId;

@Column(name = "FIRST_NAME", nullable = false,length = 50)
private String firstName;

@Column(name = "LAST_NAME", length = 50)
private String lastName;

@Embedded
private Address address = new Address();

@Column(name = "CUST_TYPE", length = 10)
private String custType;
.................
}

     在清单18所示的 OnlineCustomer 实体中,你必须指出子类所特有的属性,还要用 @PrimaryKeyJoinColumn 标注出作为外键的字段。

清单 18. 连接表继承中的子类

@Table(name="ONLINECUSTOMER")
@Entity(name = "ONLINECUSTOMER") //Name of the entity
@DiscriminatorValue("ONLINE")
@PrimaryKeyJoinColumn(name="CUST_ID",referencedColumnName="CUST_ID")
public class OnlineCustomer extends Customer{

@Column(name = "WEBSITE", length = 100)
private String website;
................
}

     在清单18中,@PrimaryKeyJoinColumn 的name属性指明了超类中的主键,referencedColumnName 指明了子类中用哪个属性去和超类做连接。对于 CustomerOnlineCustomer对象的存储或读取操作,则没有任何变化。

一表一类继承

     在一表一类继承策略中,每一个类的信息都存放在一个单独的表中,如表4所示:

表 4. 一表一类继承映射策略

ENTITY TABLE NAME Customer CUSTOMER OnlineCustomer ONLINECUSTOMER

     由于不同的实体总是存放在不同的表中,因此你无须提供 @DiscriminatorColumn 标注。同样地,由于子类和超类之间不存在任何表关联,所以 @PrimaryKeyJoinColumn 标注也是不需要的。Customer 超类的内容如清单19所示:

清单 19. 一表一类继承中的超类

@Entity(name = "CUSTOMER")
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
public class Customer implements Serializable{
@Id //signifies the primary key
@Column(name = "CUST_ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private long custId;

@Column(name = "FIRST_NAME", nullable = false,length = 50)
private String firstName;

@Column(name = "LAST_NAME", length = 50)
private String lastName;

@Embedded
private Address address = new Address();
...........
}

    如清单20所示,OnlineCustomer 和一个普通的子类没什么两样。对于 CustomerOnlineCustomer对象的存储或读取操作,同样没有任何变化。

清单 20.  一表一类继承中的子类

@Entity(name = "ONLINECUSTOMER") //Name of the entity
public class OnlineCustomer extends Customer{

@Column(name = "WEBSITE", length = 100)
private String website;
.................
}

通向更广阔的面向对象世界

     在本文中,你了解到了如何在JPA中应用面向对象中的“继承”以及回调函数。还有更多的面向对象能力可以在JPA中使用,JPA还允许你写JPQL查询语句或者原生的SQL查询,还有事务管理能力等等。

     在下一篇文章中,你将会看到JPA中的数据间的关系,和面向对象中的一样优雅。