很多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.Socket
和java.lang.Thread
。 - 如果一个持久化类继承自一个非持久化类,那么父类中的属性是不能被持久化的。
持久化单元
既然实体类已经完成,接下来就该处理 persistence.xml 配置文件了。 列表3所示的XML文件位于 META-INF 文件夹中; 它被用于指定持久化提供商的名字,实体类的名字,还有一些系统属性,例如数据库的URL、驱动、用户、密码等等。
列表 3. persistence.xml 文件样例
<?xml version="1.0"?>
<persistence>
<persistence-unit name="testjpa" transaction-type="RESOURCE_LOCAL">
<provider>
org.apache.openjpa.persistence.PersistenceProviderImpl
</provider>
<class>entity.Customer</class>
<properties>
<property name="openjpa.ConnectionURL"
value="jdbc:derby://localhost:1527/D:/OpenJPA/Derby/testdb;create=true"/>
<property name="openjpa.ConnectionDriverName"
value="org.apache.derby.jdbc.ClientDriver"/>
<property name="openjpa.ConnectionUserName" value="admin"/>
<property name="openjpa.ConnectionPassword" value="admin"/>
<property name="openjpa.Log" value="SQL=TRACE"/>
</properties>
</persistence-unit>
</persistence>
关于 persistence.xml 文件有如下重要内容需要注意:
- persistence.xml 可以包含多个持久化单元。每一个持久化单元都可以被不同的JPA提供商使用,或者能够被用来操作不同的数据库。
- JPA提供商的名字在
<provider>
标签中指定。OpenJPA 的提供商的名字是org.apache.openjpa.persistence.PersistenceProviderImpl
. - 实体类的名字在
<class>
标签中指定。 - 数据库连接属性可以在
<properties>
标签中指定。注意各个提供商之间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;
................
}
就目前的代码而言,你可以暂时不理睬 DiscriminatorColumn
和 DiscriminatorValue
标注,它们的功能我们在后面介绍。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<OnlineCustomer> 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) |
OnlineCustomer
和 Customer
的公共信息被存放在 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
指明了子类中用哪个属性去和超类做连接。对于 Customer
或 OnlineCustomer
对象的存储或读取操作,则没有任何变化。
一表一类继承
在一表一类继承策略中,每一个类的信息都存放在一个单独的表中,如表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
和一个普通的子类没什么两样。对于 Customer
或 OnlineCustomer
对象的存储或读取操作,同样没有任何变化。
清单 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中的数据间的关系,和面向对象中的一样优雅。
理解JPA,第二部分:JPA中的关系
553个读者 华丽的痘痘 @ yeeyan.com 2009年07月04日 双语对照 原文 字体大小 小 中 大
你用JAVA写的web程序非常依赖于数据之间的关系,如果你处理不好的话,结果将会变得非常糟糕。在这篇文章中,作者将向你展示,如何利用JPA的标注,在面向对象代码与关系数据之间创建一个透明的接口。最终的数据关系将会更容易管理,并且更具备可移植性。
数据对于任何一个应用程序来讲都是必不可少的,而数据之间存在的关系也具有同样的重要性。关系型数据库能够支持数据表之间的各种关系,并且还要满足完整性约束。
在这个系列文章的下半部分中,你将了解到如何使用JPA以及Java 5的标注来按照面向对象的方式处理数据间的关系。这篇文章面向的读者是那些掌握了基本的JPA概念,了解一般的关系型数据库编程,以及那些想要更深入地了解使用JPA来进行面向对象的关系设计的人们。对于JPA的简单介绍,请参考本系列文章的上半部分。
一个现实生活中的例子
假设有一家名叫XYZ的公司,为它的顾客提供5中商品,分别是A、B、C、D、E。顾客可以自由的同时订购多种商品(可以享受折扣优惠),也可以订购单一的商品。在订购商品的时候顾客不用付钱。在月底的时候,如果顾客对商品很满意,将会收到公司开出发票从而进行付款。这家公司的数据模型如图1所示,一个消费者可以下0个或多个订单,每个订单包含1个或多个商品。对于每个订单,将会产生一张发片用于付账。
图 1. 数据模型
现在XYZ公司想要调查一下顾客对他们的商品满意度如何,所以他们必须首先调查一下每一个顾客拥有多少商品。为了改进商品的质量,公司还需要对那些撤销过订单的客户做一个特别的调查。
为了实现这个目标,比较传统的方法是构建一个DAO层,然后在 CUSTOMER, ORDERS, ORDER_DETAIL, ORDER_INVOICE, 和 PRODUCT这些表之间做复杂的连接查询。这种设计模式表面上看还不错,但是随着应用程序规模的增长,很难维护和调试。
JPA提供了一种更为简洁优雅的方式来解决这一问题。我所提出的解决方案采用了面向对象的方法,同时感谢JPA,我不用写任何SQL查询,持久层供应商把这些复杂的工作都做了,这对于开发者是透明的。
你最好先从资源区下载样例代码,其中包含了本文中介绍的“1对1”,“多对1”,“1对多”还有“多对对”关系映射代码。
One-to-one 关系
首先,我们的程序要解决“订单-发票”之间的关系。对于每一个订单,都将有一个发票,同理,每一个发票也要关联到一张订单上。这两个表之间是一种 one-to-one 的映射关系,如图2所示,利用 ORDER_ID 这个外键进行关联。JPA使用 @OneToOne 这个标注来处理
one-to-one 映射关系。
图 2. A one-to-one 关系
应用程序会针对每一个发票ID来取出相应的订单数据。如清单1所示,发票实体的属性与INVOICE表的字段是完全对应的,同时发票实体还拥有一个订单对象来和 ORDER_ID 外键做关联。
清单 1. 一个样例实体,描述了 one-to-one 关系
@Entity(name = "ORDER_INVOICE")
public class Invoice {
@Id
@Column(name = "INVOICE_ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private long invoiceId;
@Column(name = "ORDER_ID")
private long orderId;
@Column(name = "AMOUNT_DUE", precision = 2)
private double amountDue;
@Column(name = "DATE_RAISED")
private Date orderRaisedDt;
@Column(name = "DATE_SETTLED")
private Date orderSettledDt;
@Column(name = "DATE_CANCELLED")
private Date orderCancelledDt;
@Version
@Column(name = "LAST_UPDATED_TIME")
private Date updatedTime;
@OneToOne(optional=false)
@JoinColumn(name = "ORDER_ID")
private Order order;
...
//getters and setters goes here
}
清单1中的 @OneToOne
和 @JoinCloumn
标注将会被持久层提供商进行内部处理,如清单2所示:
清单 2. 处理 one-to-one 关系的SQL查询
SELECT t0.LAST_UPDATED_TIME, t0.AMOUNT_PAID, t0.ORDER_ID,
t0.DATE_RAISED ,t1.ORDER_ID, t1.LAST_UPDATED_TIME, t1.CUST_ID,
t1.OREDER_DESC, t1.ORDER_DATE, t1.TOTAL_PRICE
FROM ORDER_INVOICE t0
INNER JOIN ORDERS t1 ON t0.ORDER_ID = t1.ORDER_ID
WHERE t0.INVOICE_ID = ?
清单2中的查询语句显示了 ORDERS 和 INVOICE 表之间的内连接,但是如果你需要的是一个外连接该怎么办?你可以通过修改 @OneToOne
标注的 optional
属性来很容易的设置到底采用哪种连接方式,该参数的默认值是true,意味着相关联的对象可以存在也可以不存在,从而采用外连接的方式。但是在我们的例子中,每一个订单必然会有一张发票,反之也是一样,所以我们应该把该属性的值设为false。
清单3中的代码演示了如何根据制定的发票找到相关的订单。
清单 3. 根据 one-to-one关系取出一个对象
....
EntityManager em = entityManagerFactory.createEntityManager();
Invoice invoice = em.find(Invoice.class, 1);
System.out.println("Order for invoice 1 : " + invoice.getOrder());
em.close();
entityManagerFactory.close();
....
但是如果你指定一个订单,想要取出相关的发票,这时候会发生些什么呢?
反向 one-to-one 关系
每一个关系都有两端:
- 其中一端叫 owning ,负责在数据库中更新关系。一般来讲这一端都包含外键。
- 另一端叫做 inverse ,它将映射到 owning 端。
在我们的例子中,发票对象是 owning 端,清单4展示了 inverse 端—— 也就是订单对象——的样子。
清单 4. 反向 one-to-one关系中的实体
@Entity(name = "ORDERS")
public class Order {
@Id
@Column(name = "ORDER_ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private long orderId;
@Column(name = "CUST_ID")
private long custId;
@Column(name = "TOTAL_PRICE", precision = 2)
private double totPrice;
@Column(name = "OREDER_DESC")
private String orderDesc;
@Column(name = "ORDER_DATE")
private Date orderDt;
@OneToOne(optional=false,cascade=CascadeType.ALL,
mappedBy="order",targetEntity=Invoice.class)
private Invoice invoice;
@Version
@Column(name = "LAST_UPDATED_TIME")
private Date updatedTime;
....
//setters and getters goes here
}
清单4中的代码通过 mappedBy="order" 来使得关系映射到order字段。
targetEntity
属性指明了owning类的名字。还有一个叫cascade的属性,如果你在对订单实体进行插入、更新或删除操作的时候,希望能够同时对发票实体进行操作,则需要设置这个属性。
清单5展示了如何根据制定的订单信息来找到发票对象。
Listing 5. Fetching objects involved in a bidirectional one-to-one relationship
....
EntityManager em = entityManagerFactory.createEntityManager();
Order order = em.find(Order.class, 111);
System.out.println("Invoice details for order 111 : " + order.getInvoice());
em.close();
entityManagerFactory.close();
....
Many-to-one 关系
在刚才的内容中,你学到了如何通过给定的订单来找到相关的发票信息。现在我们来看看另一个问题,如何根据某一个客户来找到相关的订单信息,反之也是一样。一个客户可以拥有0个或多个订单,但一个订单只能关联到一个客户。因此,客户和订单之间是一种 one-to-man 的关系,同理,订单和客户之间就是 many-to-one 的关系,如图3所示:
图 3. many-to-one/one-to-many 关系
现在,订单实体是owning端,将 CUST_ID 作为外键与客户实体做关联。清单6订单实体是如何处理 many-to-one 关系的。
清单 6. 实现了反向 many-to-one 关系的实体
@Entity(name = "ORDERS")
public class Order {
@Id //signifies the primary key
@Column(name = "ORDER_ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private long orderId;
@Column(name = "CUST_ID")
private long custId;
@OneToOne(optional=false,cascade=CascadeType.ALL,
mappedBy="order",targetEntity=Invoice.class)
private Invoice invoice;
@ManyToOne(optional=false)
@JoinColumn(name="CUST_ID",referencedColumnName="CUST_ID")
private Customer customer;
...............
The other attributes and getters and setters goes here
}
在清单6中,顾客实体通过 CUST_ID 作为外键连接到订单实体。在这里 optional 属性的值仍然为 false
,因为每个订单必须有一个相关联的顾客。现在,订单实体已经和两个实体做了关联,分别是与发票实体的 one-to-one 关联,以及和顾客实体的 many-to-one 关联。
清单7展示了如何根据一个特定的订单来找到相关联的顾客。
清单 7. 利用 many-to-one 关系来取得一个对象
........
EntityManager em = entityManagerFactory.createEntityManager();
Order order = em.find(Order.class, 111);
System.out.println("Customer details for order 111 : " + order.getCustomer());
em.close();
entityManagerFactory.close();
........
但是如果给定一个顾客,想取出相关联的订单信息该怎么办?
One-to-many 关系
只要owning端进行了恰当的设计,那么根据顾客来查找相关订单信息是非常容易的。在前面的内容中,你看到订单实体被设计为owning端,拥有一个 many-to-one 关系,而反过来的话则变成一个 one-to-many 的关系。清单8展示了这种 one-to-many 关系是如何来定义的。
清单 8. 包含了 one-to-many 关系的实体
@Entity(name = "CUSTOMER")
public class Customer {
@Id //signifies the primary key
@Column(name = "CUST_ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private long custId;
@Column(name = "FIRST_NAME", length = 50)
private String firstName;
@Column(name = "LAST_NAME", nullable = false,length = 50)
private String lastName;
@Column(name = "STREET")
private String street;
@OneToMany(mappedBy="customer",targetEntity=Order.class,
fetch=FetchType.EAGER)
private Collection orders;
...........................
// The other attributes and getters and setters goes here
}
在清单8中的 @OneToMany
标注中,出现了一个新的属性:fetch。对于 one-to-many 关系来讲,默认的fetch策略是LAZY。 FetchType.LAZY
是JPA的默认设置,表明程序将延迟加载相关信息,直到你真正访问这些信息的时候才进行加载,这就是所谓的 lazy loading。延迟加载是完全透明的,当你访问相关内容的时候,程序自动访问数据库来为你获取相关信息。另外一种fetch策略是 FetchType.EAGER
,他的含义是无论何时只要你取得了一个实体,那么这个实体中所有的字段都将立刻从数据库中取出。如果想要使用EAGER策略,那么你必须明确指定fetch=FetchType.EAGER 。清单9中的代码是根据特定的顾客来取出相关的订单信息。
清单 9. 根据 one-to-many 关系来取出对象
........
EntityManager em = entityManagerFactory.createEntityManager();
Customer customer = em.find(Customer.class, 100);
System.out.println("Order details for customer 100 : " + customer.getOrders());
em.close();
entityManagerFactory.close();
.........
Many-to-many 关系
还有最后一种关系映射需要考虑,一个订单可以包含多种商品,同时一个商品也可以出现在一个或多个订单当中,这就是所谓的 many-to-many 关系,如图4所示:
图 4. many-to-many 关系
为了定义 many-to-many 关系,我们的程序需要引入一个名叫 ORDER_DETAI 的连接表来保存订单和商品之间的联系,如清单10所示:
清单 10. 包含 many-to-many关系的实体
@Entity(name = "ORDERS")
public class Order {
@Id
@Column(name = "ORDER_ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private long orderId;
@Column(name = "CUST_ID")
private long custId;
@Column(name = "TOTAL_PRICE", precision = 2)
private double totPrice;
@OneToOne(optional=false,cascade=CascadeType.ALL, mappedBy="order",
targetEntity=Invoice.class)
private Invoice invoice;
@ManyToOne(optional=false)
@JoinColumn(name="CUST_ID",referencedColumnName="CUST_ID")
private Customer customer;
@ManyToMany(fetch=FetchType.EAGER)
@JoinTable(name="ORDER_DETAIL",
joinColumns=
@JoinColumn(name="ORDER_ID", referencedColumnName="ORDER_ID"),
inverseJoinColumns=
@JoinColumn(name="PROD_ID", referencedColumnName="PROD_ID")
)
private List<Product> productList;
...............
The other attributes and getters and setters goes here
}
@JoinTable
标注用于指定数据库中的一个表来保存订单ID和商品ID之间的联系。指定了 @JoinTable
的实体是owning端。在我们这个例子中,订单实体是owning端。
清单11展示了如何根据订单取得相关的商品信息。
清单 11. 利用 many-to-many 关系取得信息
..........
EntityManagerFactory entityManagerFactory =
Persistence.createEntityManagerFactory("testjpa");
EntityManager em = entityManagerFactory.createEntityManager();
Order order = em.find(Order.class, 111);
System.out.println("Product : " + order.getProductList());
em.close();
entityManagerFactory.close();
..........
反向 many-to-many 关系
一个商品可以被包含在多个订单当中。一旦你已经在owning端做好了映射,那么inverse端的映射将变得非常容易,如清单12所示:
清单 12. 拥有反向 many-to-many 关系的实体
@Entity(name = "PRODUCT")
public class Product {
@Id
@Column(name = "PROD_ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private long prodId;
@Column(name = "PROD_NAME", nullable = false,length = 50)
private String prodName;
@Column(name = "PROD_DESC", length = 200)
private String prodDescription;
@Column(name = "REGULAR_PRICE", precision = 2)
private String price;
@Column(name = "LAST_UPDATED_TIME")
private Date updatedTime;
@ManyToMany(mappedBy="productList",fetch=FetchType.EAGER)
private List<Order> orderList;
...............
The other attributes and getters and setters goes here
}
清单13展示了如何根据商品来取得相关的订单信息。
清单 13. 利用反向 many-to-many 关系取得数据
..........
EntityManagerFactory entityManagerFactory =
Persistence.createEntityManagerFactory("testjpa");
EntityManager em = entityManagerFactory.createEntityManager();
Product product = em.find(Product.class, 2000);
System.out.println("Order details for product : " + product.getOrderList());
em.close();
entityManagerFactory.close();
..........
最终展示
现在,请整理一下你的思路,回顾一下本文开头讲过的内容,这个公司XYZ想要查找如下信息:
- 每一个顾客所购买的商品数目
- 每一个顾客都撤消了哪些商品
既然关系已经定义好了,只需要几行代码就能够实现这些功能,请看清单14:
清单 14. 把所有的关系综合到一起
..............
Query query = em.createQuery("SELECT customer FROM CUSTOMER customer");
List list= query.getResultList();
for(Customer customer:list){
List prodList = new ArrayList();
List prodListCancelled = new ArrayList();
if(customer.getOrders()!=null){
for(Order allOrders: customer.getOrders()){
if(allOrders.getInvoice().getOrderCancelledDt() == null){
//Find out how many products each customer has
prodList.addAll(allOrders.getProductList());
}else{
//Find out how many products cancelled by each customer
prodListCancelled.addAll(allOrders.getProductList());
}
}
}
}
..............
清单14的代码中,我们从CUSTOMER表中取出了所有的顾客信息。对于每一个顾客,我们取出订单的数量,然后过滤掉那些没有被撤销的订单,然后把这些内容添加到 prodList
中。同时我们每一个被用户撤销的订单中的商品数目,然后把这些信息添加到 prodListCancelled
中。
因此 prodList
包含了用户使用中的商品,而 prodListCancelled
包含了用户撤销的商品。有这些数据在手,XYZ公司可以很容易地知道顾客最喜欢的商品是什么。
解密数据关系
本文的样例程序被设计成展示各种各样的数据关系。在本文中,你能够看到在多种关系表之间使用JPA的面向对象能力来处理复杂的CRUD操作。这样就能够彻底降低企业级应用程序在查询方面的复杂度,使得程序更容易维护。
JPA是一种简单的标准的数据持久层API,在JAVA SE 和 JAVA EE中都能够使用。希望这个简单的例子能够帮助你更好的学习JPA。