企业级Java

JPA入门教程 – 终极指南

编者按:Java持久化APIJPA)是Java语言中的应用程序编程接口规范,在基于Java平台标准版和Java平台企业版的应用程序中,JPA负责管理关系数据。

JPA已经成为事实上的标准,用于编写与数据库交互的代码。也正由于此,我们已经在Java Code Geeks提供了丰富的教程,可以在这里访问这些教程。此外,我们还制作了一本JPA迷你书,它能够帮助你入门JPA,而且顺利过渡到更高级的概念(加入我们的newsletter,可以免费获取)。

现在,我们想要建立一篇独立的、可做参考的文章:提供一个帮助你了解如何使用JPA的框架;并帮助你快速启动你的JPA应用程序开发。开始享受吧!

1. 简介

Java持久化API(JPA)是一个独立于供应商的、用于映射Java对象和关系型数据库表的规范。此规范的实现,使得应用程序的开发者们可以不依赖于他们工作中面对的特定数据库产品,从而开发出可以与不同数据库产品良好工作的CRUD(创建、读取、更新、删除)操作代码。这些框架除了可以用于处理与数据库交互的代码(JDBC代码),也可以用于映射数据和应用程序中的对象。

JPA由三个不同的组件构成:

  • 实体(Entities): 在当前版本的JPA中实体是普通Java对象(POJO)。老版本的JPA中实体类需要继承JPA提供的实体基类,但是这样的设计导致框架中存在了严重的依赖关系,测试变得更加困难;所以在新版JPA中不再要求实体类继承任何框架类。
  • 对象-关系型元数据(Object-relational metadata): 应用程序的开发者们必须正确设定Java类和它们的属性与数据库中的表和列的映射关系。有两种设定方式:通过特定的配置文件建立映射;或者使用在新版本中支持的注解。
  • Java持久化查询语句(Java Persistence Query Language - JPQL): 因为JPA旨在建立不依赖于特定的数据库的抽象层,所以它也提供了一种专有查询语言来代替SQL。 这种由JPQL到SQL语言的转换,为JPA提供了支持不同数据库方言的特性,使得开发者们在实现查询逻辑时不需要考虑特定的数据库类型。

在本教程中,我们会纵览JPA框架的不同方面,同时开发一个简单的、可以从关系数据库中存储和检索数据的Java SE应用程序。我们使用到的类库/环境如下:

  • Maven >= 3.0 :构建环境
  • JPA 2.1 :包含于Java企业版(JEE)7.0
  • Hibernate :JPA实现 (4.3.8.Final)
  • H2 :关系型数据库v3.176

2. 创建项目

第一步我们将通过命令行创建一个简单的Maven项目:

mvn archetype:create -DgroupId=com.javacodegeeks.ultimate -DartifactId=jpa

这个命令将会建立如下所示的目录结构:

|-- src
|   |-- main
|   |   `-- java
|   |       `-- com 
|   |           `-- javacodegeeks
|   |			     `-- ultimate
|   `-- test
|   |   `-- java
|   |       `-- com 
|   |           `-- javacodegeeks
|   |			     `-- ultimate
`-- pom.xml

如下更新pom.xml,将项目中需要的类库添加到依赖(dependencies)设置部分:

<properties>
	<jee.version>7.0</jee.version>
	<h2.version>1.3.176</h2.version>
	<hibernate.version>4.3.8.Final</hibernate.version>
</properties>

<dependencies>
	<dependency>
		<groupId>javax</groupId>
		<artifactId>javaee-api</artifactId>
		<version>${jee.version}</version>
		<scope>provided</scope>
	</dependency>
	<dependency>
		<groupId>com.h2database</groupId>
		<artifactId>h2</artifactId>
		<version>${h2.version}</version>
	</dependency>
	<dependency>
		<groupId>org.hibernate</groupId>
		<artifactId>hibernate-entitymanager</artifactId>
		<version>${hibernate.version}</version>
	</dependency>
</dependencies>

为了更清晰的管理不同类库的版本信息,我们使用Maven属性设定这些类库的版本信息,然后在pom.xml中的依赖设置部分引用这些属性。

3. 基础知识

3.1. EntityManager和持久化单元(Persistence Unit)

现在我们可以开始实现我们的第一个JPA功能了。首先我们创建一个简单的类,它有一个run()方法,主程序的main()方法会调用到它:

public class Main {
	private static final Logger LOGGER = Logger.getLogger("JPA");

	public static void main(String[] args) {
		Main main = new Main();
		main.run();
	}

	public void run() {
		EntityManagerFactory factory = null;
		EntityManager entityManager = null;
		try {
			factory = Persistence.createEntityManagerFactory("PersistenceUnit");
			entityManager = factory.createEntityManager();
			persistPerson(entityManager);
		} catch (Exception e) {
			LOGGER.log(Level.SEVERE, e.getMessage(), e);
			e.printStackTrace();
		} finally {
			if (entityManager != null) {
				entityManager.close();
			}
			if (factory != null) {
				factory.close();
			}
		}
	}
	...

几乎所有与JPA交互的操作都是通过EntityManager完成的。要获得一个EntityManager的实例,首先需要创建一个EntityManagerFactory的实例。通常情况下我们在每个应用中的“持久化单元”只需要一个EntityManagerFactory。持久化单元是通过数据库配置文件persistence.xml归集到一起的一组JPA类:

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
 http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">

    <persistence-unit name="PersistenceUnit" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <property name="connection.driver_class" value="org.h2.Driver"/>
            <property name="hibernate.connection.url" value="jdbc:h2:~/jpa"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            <property name="hibernate.hbm2ddl.auto" value="create"/>
            <property name="hibernate.show_sql" value="true"/>
			<property name="hibernate.format_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

这个文件位于maven项目的src/main/resource/META-INF目录。可以看到,我们定义了一个名为PersistentUnit、transaction-type是RESOURCE_LOCAL的持久化单元(persistence-unit)。 Transaction-type用于定义在应用程序中如何处理事务。

在我们的示例程序中我们想要自己来控制、处理事务,所以这里设置它的值为RESOURCE_LOCAL。 在使用JEE容器时,所使用的容器负责创建EntityManagerFactory, 然后它会提供EntityManager供我们使用。容器同时也负责处理每个事务的开始和结束。在本例中我们设置其值为JTA。

通过设置provider的值为org.hibernate.ejb.HibernatePersistence,表明我们想要在此使用的JPA实现。由于之前我们已经在classpath中通过定义Hibernate的依赖而包含了Hibernate作为JPA的提供者,在这里我们可以直接引用它的类名来设定提供商。

Persistence.xml中的下一步是告知JPA提供商我们想要使用的数据库,设定Hibernate需要使用的JDBC驱动即可。此例我们使用的是H2数据库(www.h2database.com),所以设置属性connection.driver_class为org.h2.Driver。

为使Hibernate能够创建数据库连接,我们还需要提供数据库连接URL。H2提供的连接选项是创建单文件数据库,所以只需在JDBC URL中设置这个文件的路径就好了。因此,JDBC URL设定为jdbc:h2:~/jpa,告诉H2在用户的主目录中创建一个名为jps.h2.db的文件。

H2是一个快速的开源的数据库,它由一个大小仅为1.5MB的单个jar文件组成,Java应用程序可以很容易的嵌入它。我们在本例想要创建一个简单的示例程序来演示JPA的用法,H2正是方便使用。在一些涉及到海量数据、需要良好扩展性的系统解决方案中,你可能需要选择其它不同的数据库产品,但是对于小数据量、强关系型的数据库,H2是个不错的选择。

下一个Hibernate设置是它要使用的JDBC方言。由于Hibernate已经为H2提供了专有的方言实现,我们这里将其值设置到属性hibernate.dialect。使用这一方言实现,Hibernate能够基于H2数据库生成合适的SQL语句。

最后但同样重要的一点,我们提供了三个方便实用的供开发使用的属性,这些属性不会使用在生产环境。第一个是属性hibernate.hbm2ddl.auto,用于告知Hibernate在系统启动时从头开始创建所有的表。如果表已经存在,就删除它。在我们的示例程序中它非常有用;因为需要保证,1)系统启动时数据库是空的, 2)我们从上次应用启动后对schema做的所有改动都能够正确反应到数据库。

第二个选项是hibernate.show_sql,用于告诉Hibernate在命令行打印出所有发送到数据库的SQL语句。开启这个选项后,我们可以很容易的追踪所有SQL语句,检查程序是否如我们预期的那样工作。

最后一个选项是可以设置属性hibernate.format_sql为true,告诉Hibernate打印出更漂亮的可读性更好的SQL语句。

现在,我们已经配置好persistence.xml文件,我们继续看之前创建的Java代码:

EntityManagerFactory factory = null;
EntityManager entityManager = null;
try {
	factory = Persistence.createEntityManagerFactory("PersistenceUnit");
	entityManager = factory.createEntityManager();
	persistPerson(entityManager);
} catch (Exception e) {
	LOGGER.log(Level.SEVERE, e.getMessage(), e);
	e.printStackTrace();
} finally {
	if (entityManager != null) {
		entityManager.close();
	}
	if (factory != null) {
		factory.close();
	}
}

如上,获取一个EntityManagerFactory实例,通过它获取一个EntityManager实例,然后就可以使用它们在方法persistPerson()中保存数据到数据库。请注意,当我们处理完毕所有工作后,我们必须关闭掉EntityManager和EntityManagerFactory。

3.2. 事务(Transactions)

EntityManager代表一个持久化单元,因此在RESOURCE_LOCAL的应用程序中我们只需要一个EntityManager实例。一个持久化单元就是一个缓存,用于存储那些数据库中所存储的各实体的状态。存储数据至数据库时,我们将它传递给EntityManager,随后传递给下层的缓存。如果你想在数据库中插入一条新数据的话,可以调用EntityManager的persist()方法,来看下面的演示代码:

private void persistPerson(EntityManager entityManager) {
	EntityTransaction transaction = entityManager.getTransaction();
	try {
		transaction.begin();
		Person person = new Person();
		person.setFirstName("Homer");
		person.setLastName("Simpson");
		entityManager.persist(person);
		transaction.commit();
	} catch (Exception e) {
		if (transaction.isActive()) {
			transaction.rollback();
		}
	}
}

不过在调用persist()方法之前,我们需要调用Transaction对象(通过EntityManager获取得到)的transaction.begin()方法开启一个新事务。如果我们遗漏了这个步骤的话,Hibernate会抛出IllegalStateException异常,提醒我们忘了在事务中执行persist()操作:

java.lang.IllegalStateException: Transaction not active
	at org.hibernate.jpa.internal.TransactionImpl.commit(TransactionImpl.java:70)
	at jpa.Main.persistPerson(Main.java:87)

调用persist()方法后,我们需要提交事务,即发送数据到数据库并存储。如果在try代码块中有异常抛出,我们必须回滚之前开启的事务。但是由于我们只能回滚活动的事务,所以在回滚前,我们需要检查当前事务是否已在运行,因为所发生的异常有可能是在调用transaction.begin()时发生的。

3.3. 数据库表(Tables)

通过注解@Entity,将类Person映射到数据库表T_PERSON:

@Entity
@Table(name = "T_PERSON")
public class Person {
	private Long id;
	private String firstName;
	private String lastName;

	@Id
	@GeneratedValue
	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	@Column(name = "FIRST_NAME")
	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	@Column(name = "LAST_NAME")
	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}
}

附加的注解@Table是可选的,不过你可以用它来指定一个特定的表名。在我们的示例中,我们想要所有的表都有前缀T_,所以我们设定了表名T_PERSON。表T_PERSON有3列:ID,FIRST_NAME,LAST_NAME。

这些信息是通过注解@Column和它的属性name提供给JPA提供商的。不过这些注解都是可选的。JPA会为Java类中所有具有setter和getter方法的属性创建数据库列,唯一的例外是具有显式@Transient注解声明的属性。另外,你可以使用@Column注解提供的其它属性为每个列指定更多的信息:

@Column(name = "FIRST_NAME", length = 100, nullable = false, unique = false)

上例中的代码指明了:限制这个字符串长度为100个字符;该列不能包含空值(null);不必是唯一的。如果试图将空值(null)作为first name插入数据库表的话,就会触发数据库约束冲突,进而导致当前事务回滚。

注解@Id和@GeneratedValue用于告诉JPA该值是主键,而且会自动生成。

在上面的示例代码中,对于每个需要映射到数据库列的字段,其对应的getter方法都加上了JPA注解。另外一种方式是直接在每个类字段上添加注解,而不是其getter方法上:

@Entity
@Table(name = "T_PERSON")
public class Person {
	@Id
	@GeneratedValue
	private Long id;
	@Column(name = "FIRST_NAME")
	private String firstName;
	@Column(name = "LAST_NAME")
	private String lastName;
	...

这两种方式基本是等价的,唯一的不同是当你需要在子类中覆写父类某些字段的注解时,它们所扮演的角色不同。在本教程的后面章节我们会看到,我们可以继承实体类,同时扩展其字段。如果我们在字段级别定义了JPA注解的话,就不能通过覆写它的对应getter方法来达到覆写它的目的。

有一点需要注意的是,在同一个实体层次结构中,我们必须保持同一种使用注解的方式。在一个JPA项目中,你可以在字段和方法中混用这两种注解方式,但是在一个实体及其子类中必须保证注解方式的一致性。如果你需要在其子类结构中换用另一种注解方式,可以使用JPA注解@Access来指明这一个特定的子类使用了另一种不同的注解方式来注解其字段和方法:

@Entity
@Table(name = "T_GEEK")
@Access(AccessType.PROPERTY)
public class Geek extends Person {
...

上面这个代码段告诉JPA这个类在方法层面使用注解,即使它的父类有可能在字段层面使用了注解。

运行上面的代码,Hibernate会在我们的本地H2数据库上执行如下操作:

Hibernate: drop table T_PERSON if exists
Hibernate: create table T_PERSON (id bigint generated by default as identity, FIRST_NAME varchar(255), LAST_NAME varchar(255), primary key (id))
Hibernate: insert into T_PERSON (id, FIRST_NAME, LAST_NAME) values (null, ?, ?)

我们可以看到,如果T_PERSON表已经存在的话,Hibernate会首先删除随后再重建它。 它创建了两个varchar(255)类型的列(FIRST_NAME, LAST_NAME)和一个bigint类型的列(id)。

后者(id列)被定义为主键,当我们往数据库插入新值时,会自动生成id值。

我们可以使用H2附带的Shell工具检查是否达到我们预期的结果。我们需要归档文件h2-1.3.176.jar来使用Shell:

>java -cp h2-1.3.176.jar org.h2.tools.Shell -url jdbc:h2:~/jpa

...

sql> select * from T_PERSON;
ID | FIRST_NAME | LAST_NAME
1  | Homer      | Simpson
(4 rows, 4 ms)

上面的查询结果显示了表T_PERSON包含了一条记录,id值为1,列first name和last name也都含有正确的值。

4. 继承(Inheritance)

完成项目的设置和上一节中的简单用例之后,我们来看一些更加复杂的用例。假设我们现在想要存储Geek们的个人信息以及他们最喜爱的编程语言信息。由于Geek也是Person,所以我们在Java模式实现中看到它是Person的子类:

@Entity
@Table(name = "T_GEEK")
public class Geek extends Person {
	private String favouriteProgrammingLanguage;
	private List<Project> projects = new ArrayList<Project>();

	@Column(name = "FAV_PROG_LANG")
	public String getFavouriteProgrammingLanguage() {
			return favouriteProgrammingLanguage;
	}

	public void setFavouriteProgrammingLanguage(String favouriteProgrammingLanguage) {
		this.favouriteProgrammingLanguage = favouriteProgrammingLanguage;
	}
	...
}

首先在Geek类设置注解@Entity和@Table;然后使用Hibernate创建新表T_GEEK:

Hibernate: create table T_PERSON (DTYPE varchar(31) not null, id bigint generated by default as identity, FIRST_NAME varchar(255), LAST_NAME varchar(255), FAV_PROG_LANG varchar(255), primary key (id))

我们可以看到Hibernate为这两个实体创建了一个表;新增的名为DTYPE的列用于存储标志位,标识我们存储的是Person还是Geek。我们来添加一些用于持久化Geek到数据库的逻辑(为了更好的可读性,我省略了捕获异常和回滚事务的代码):

private void persistGeek(EntityManager entityManager) {
	EntityTransaction transaction = entityManager.getTransaction();
	transaction.begin();
	Geek geek = new Geek();
	geek.setFirstName("Gavin");
	geek.setLastName("Coffee");
	geek.setFavouriteProgrammingLanguage("Java");
	entityManager.persist(geek);
	geek = new Geek();
	geek.setFirstName("Thomas");
	geek.setLastName("Micro");
	geek.setFavouriteProgrammingLanguage("C#");
	entityManager.persist(geek);
	geek = new Geek();
	geek.setFirstName("Christian");
	geek.setLastName("Cup");
	geek.setFavouriteProgrammingLanguage("Java");
	entityManager.persist(geek);
	transaction.commit();
}

这个方法执行后,表T_PERSON中含有如下记录(也包含我们之前已经插入的Person记录):

sql> select * from t_person;
DTYPE  | ID | FIRST_NAME | LAST_NAME | FAV_PROG_LANG
Person | 1  | Homer      | Simpson   | null
Geek   | 2  | Gavin      | Coffee    | Java
Geek   | 3  | Thomas     | Micro     | C#
Geek   | 4  | Christian  | Cup       | Java

正如预期的那样,新列DTYPE代表着是哪一种类型的Person。在DTYPE不是Geek的记录中,列FAV_PROG_LANG的值为null。

如果你不喜欢类型列的当前名字和类型,可以使用对应的注解更改它。如下所示,我们想要让这个列的名字改为PERSON_TYPE,类型由原来的string改为integer:

@DiscriminatorColumn(name="PERSON_TYPE", discriminatorType = DiscriminatorType.INTEGER)

再次查询会得到如下结果:

sql> select * from t_person;
PERSON_TYPE | ID | FIRST_NAME | LAST_NAME | FAV_PROG_LANG
-1907849355 | 1  | Homer      | Simpson   | null
2215460     | 2  | Gavin      | Coffee    | Java
2215460     | 3  | Thomas     | Micro     | C#
2215460     | 4  | Christian  | Cup       | Java

并不是在所有情况下你都会想在同一张表中存储所有不同类型的实体数据,特别是当不同的实体类型含有很多的不同列时。因此JPA允许你指定如何布局不同的列。有三种选项可供选择:

  • SINGLE_TABLE: 这种策略映射所有的类到一个单一的表。其结果是,每一行都含有所有类型的所有列;如果有空列的话,数据库就需要额外的存储空间。 另一方面来看这种策略所带来的优点是:所有的查询都不需要使用连接,从而可以更快的运行。
  • JOINED: 这种策略为每种类型创建一个单独的表。因此每个表只包含它所映射的实体的状态。加载实体时,JPA供应商需要从当前实体映射的所有表中加载相应的数据。这种方法减少了存储空间,但从另一方面来看它引入了连接查询,这会显著降低查询速度。
  • TABLE_PER_CLASS: 和JOINED 策略类似,这个策略为每种实体类型创建单独的表。但与JOINED策略相反的是, 这些表包含了所有与当前实体相关的信息。因此加载这些实体时不需要引入连接查询,但它带来的新问题是:在不知道具体的子类时,需要使用另外的SQL查询来确定它的信息。

更改我们的实现类开始使用JOINED策略,只需要在基类中添加如下注解:

@Inheritance(strategy = InheritanceType.JOINED)

现在Hibernate为Person和Geek创建了两个表:

Hibernate: create table T_GEEK (FAV_PROG_LANG varchar(255), id bigint not null, primary key (id))
Hibernate: create table T_PERSON (id bigint generated by default as identity, FIRST_NAME varchar(255), LAST_NAME varchar(255), primary key (id))

添加一些数据到Person和Geek后我们查询数据库可以得到如下结果:

sql> select * from t_person;
ID | FIRST_NAME | LAST_NAME
1  | Homer      | Simpson
2  | Gavin      | Coffee
3  | Thomas     | Micro
4  | Christian  | Cup
(4 rows, 12 ms)
sql> select * from t_geek;
FAV_PROG_LANG | ID
Java          | 2
C#            | 3
Java          | 4
(3 rows, 7 ms)

正如预期这些数据分布在两个表中。基表T_PERSON包含了所有共有的属性,而表T_GEEK只包含每个geek的对应数据。每一行数据通过列ID指向一个Person记录。

当我们发出Person记录查询时,数据库会收到如下SQL:

select
	person0_.id as id1_2_,
	person0_.FIRST_NAME as FIRST_NA2_2_,
	person0_.LAST_NAME as LAST_NAM3_2_,
	person0_1_.FAV_PROG_LANG as FAV_PROG1_1_,
	case 
		when person0_1_.id is not null then 1 
		when person0_.id is not null then 0 
	end as clazz_ 
from
	T_PERSON person0_ 
left outer join
	T_GEEK person0_1_ 
		on person0_.id=person0_1_.id

可以看到,必须要加上连接查询才能够获取到表T_GEEK中的数据,而且Hibernate通过返回一个整数(查看case语句)来指明当前行是否是Geek信息。

来看Java代码中如何完成这样的查询:

TypedQuery<Person> query = entityManager.createQuery("from Person", Person.class);
List<Person> resultList = query.getResultList();
for (Person person : resultList) {
	StringBuilder sb = new StringBuilder();
	sb.append(person.getFirstName()).append(" ").append(person.getLastName());
	if (person instanceof Geek) {
		Geek geek = (Geek)person;
		sb.append(" ").append(geek.getFavouriteProgrammingLanguage());
	}
	LOGGER.info(sb.toString());
}

首先,我们调用EntityManager的createQuery()方法创建一个Query对象。查询子句可以省略select关键字。第二个参数用于参数化这个方法,这里的Query面向Person类型。调用query.getResultList()执行查询请求。查询返回的结果列表是可迭代的,因此我们可以直接迭代遍历Person对象。如果我们需要判断当前对象到底是Person还是Geek,可以使用Java中的instanceof运算符。

运行上面的代码会得到如下输出:

Homer Simpson
Gavin Coffee Java
Thomas Micro C#
Christian Cup Java

5. 实体关系(Relationships)

目前为止,除了已经提到过的子类和其父类之间的扩展(extends)关系外,我们还没有介绍任何不同实体间的模型关系。JPA为建模中涉及到的实体/表提供了多种关系:

  • OneToOne: 在这种关系中每个实体只含有一个明确的对其它实体的引用;反之亦然。
  • OneToMany / ManyToOne: 在这种关系中,一个实体可以有多个子实体,每个子实体只属于一个父实体。
  • ManyToMany: 在这种关系中,一种类型的多个实体,可以含有其它类型实体的多个引用。
  • Embedded: 在这种关系中,其它实体是和其父实体存储在同一个表中(即,每一个表都有两个实体)。
  • ElementCollection: 这种关系类似于OneToMany关系,但不同的是,它的引用实体是Embedded实体。这样我们就可以在简单对象上定义OneToMany关系,而不必定义在另外的表中使用的“普通”Embedded关系。

5.1. 一对一(OneToOne)

首先来看OneToOne关系,如下所示增加一个新的实体IdCard:

@Entity
@Table(name = "T_ID_CARD")
public class IdCard {
	private Long id;
	private String idNumber;
	private Date issueDate;

	@Id
	@GeneratedValue
	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	@Column(name = "ID_NUMBER")
	public String getIdNumber() {
		return idNumber;
	}

	public void setIdNumber(String idNumber) {
		this.idNumber = idNumber;
	}

	@Column(name = "ISSUE_DATE")
	@Temporal(TemporalType.TIMESTAMP)
	public Date getIssueDate() {
		return issueDate;
	}

	public void setIssueDate(Date issueDate) {
		this.issueDate = issueDate;
	}
}

请注意这里我们使用了通用类java.util.Date来定义IdCard的签发日期。我们可以使用注解@Temporal告诉JPA我们想如何序列化Date信息到数据库中。根据底层数据库产品的不同,这个列映射为一个相应的日期/时间戳类型。这个注解的可能值是:TIMESTAMP, TIME和DATE。

下面定义告诉JPA每个Person含有一个确定的IDCard:

@Entity
@Table(name = "T_PERSON")
public class Person {
	...
	private IdCard idCard;
	...

	@OneToOne
	@JoinColumn(name = "ID_CARD_ID")
	public IdCard getIdCard() {
		return idCard;
	}

在表T_PERSON中,有一个另外的列ID_CARD_ID,用来存储指向表T_ID_CARD的外键。 Hibernate会如下所示生成这两个表:

    create table T_ID_CARD (
        id bigint generated by default as identity,
        ID_NUMBER varchar(255),
        ISSUE_DATE timestamp,
        primary key (id)
    )

    create table T_PERSON (
        id bigint generated by default as identity,
        FIRST_NAME varchar(255),
        LAST_NAME varchar(255),
        ID_CARD_ID bigint,
        primary key (id)
    )

一个很重要的事实是我们可以定义何时加载IDCard的实体。因此我们可以在注解@OneToOne中增加属性fetch:

@OneToOne(fetch = FetchType.EAGER)

FetchType.EAGER是其默认值,它表示我们每次加载一个Person时也要同时加载IdCard。换句话说我们也可以设置其加载方式为当我们通过person.getIdCard()访问时才加载它:

@OneToOne(fetch = FetchType.LAZY)

这样的话当我们加载所有Person时就会产生如下SQL语句:

Hibernate: 
    select
        person0_.id as id1_3_,
        person0_.FIRST_NAME as FIRST_NA2_3_,
        person0_.ID_CARD_ID as ID_CARD_4_3_,
        person0_.LAST_NAME as LAST_NAM3_3_,
        person0_1_.FAV_PROG_LANG as FAV_PROG1_1_,
        case 
            when person0_1_.id is not null then 1 
            when person0_.id is not null then 0 
        end as clazz_ 
    from
        T_PERSON person0_ 
    left outer join
        T_GEEK person0_1_ 
            on person0_.id=person0_1_.id
Hibernate: 
    select
        idcard0_.id as id1_2_0_,
        idcard0_.ID_NUMBER as ID_NUMBE2_2_0_,
        idcard0_.ISSUE_DATE as ISSUE_DA3_2_0_ 
    from
        T_ID_CARD idcard0_ 
    where
        idcard0_.id=?

可以看到现在我们必须分别加载每个IDCard。所以我们必须谨慎使用这个特性,因为在你加载很多person数据时它会导致数以百计的额外的查询请求,而且你要牢记需要单独加载每个IDCard。

5.2. 一对多(OneToMany)

另外一个重要的关系就是@OneToMany关系。在我们的示例中每个Person都拥有一个或多个手机(Phone):

@Entity
@Table(name = "T_PHONE")
public class Phone {
	private Long id;
	private String number;
	private Person person;

	@Id
	@GeneratedValue
	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	@Column(name = "NUMBER")
	public String getNumber() {
		return number;
	}

	public void setNumber(String number) {
		this.number = number;
	}

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "PERSON_ID")
	public Person getPerson() {
		return person;
	}

	public void setPerson(Person person) {
		this.person = person;
	}
}

每一个Phone除了有手机号码(number)外还有一个内部ID。我们还需要在Phone上指明一个@ManyToOne的关系,因为“一个”Person可能拥有“多个”Phone。注解@JoinColumn用于指明表T_PHONE中用来存储对应Person表外键的列。

关于此关系的另外一点是,我们需要在Person中添加一个Phone对象的集合(List),并且在它的getter方法上加上注解@OneToMany,因为“一个”Person可能拥有“多个”Phone:

private List<Phone> phones = new ArrayList<>();
...
@OneToMany(mappedBy = "person", fetch = FetchType.LAZY)
public List<Phone> getPhones() {
	return phones;
}

属性mappedBy的值告诉JPA这个注解在关系的另一端(这里是Phone.person)所引用的集合。

由于我们并不想每次加载一个Person对象时也去同时加载它的所有Phone对象,所以设置这个关系的加载方式为懒加载(lazy,虽然这就是它的默认值,这里我们还是显式设置这个值)。现在,在我们每次为Person对象加载Phone对象集合时,会得到一个额外的查询语句:

select
	phones0_.PERSON_ID as PERSON_I3_3_0_,
	phones0_.id as id1_4_0_,
	phones0_.id as id1_4_1_,
	phones0_.NUMBER as NUMBER2_4_1_,
	phones0_.PERSON_ID as PERSON_I3_4_1_ 
from
	T_PHONE phones0_ 
where
	phones0_.PERSON_ID=?

由于属性fetch的值是在编译期设定的,很遗憾我们不能在运行时改变它。但是,如果我们确定只会在当前用例加载所有Phone对象而不会在其它用例这么做,可以设置关系为懒加载,并且在JPQL查询中加入left join fetch子句;这样做的话,就可以保证即使已经设置关系为FetchType.LAZY,JPA提供商依然会在这个特定的查询中加载所有的Phone对象。来看一个这种查询的示例:

TypedQuery<Person> query = entityManager.createQuery("from Person p left join fetch p.phones", Person.class);

我们设置了Person的别名为p,同时告诉JPA加载每个Person拥有的所有Phone实例。 这样在Hibernate中会生成如下查询:

    select
        person0_.id as id1_3_0_,
        phones1_.id as id1_4_1_,
        person0_.FIRST_NAME as FIRST_NA2_3_0_,
        person0_.ID_CARD_ID as ID_CARD_4_3_0_,
        person0_.LAST_NAME as LAST_NAM3_3_0_,
        person0_1_.FAV_PROG_LANG as FAV_PROG1_1_0_,
        case 
            when person0_1_.id is not null then 1 
            when person0_.id is not null then 0 
        end as clazz_0_,
        phones1_.NUMBER as NUMBER2_4_1_,
        phones1_.PERSON_ID as PERSON_I3_4_1_,
        phones1_.PERSON_ID as PERSON_I3_3_0__,
        phones1_.id as id1_4_0__ 
    from
        T_PERSON person0_ 
    left outer join
        T_GEEK person0_1_ 
            on person0_.id=person0_1_.id 
    left outer join
        T_PHONE phones1_ 
            on person0_.id=phones1_.PERSON_ID

请注意,如果这里没有关键字left(即,只有join fetch)的话,Hibernate只会生成内连接(inner join),也就是只会加载那些拥有一个及以上手机号码的Person对象。

5.3. 多对多(ManyToMany)

另一个有意思的关系是@ManyToMany。因为一个Geek可以加入很多项目(Project)而且一个Project包含着很多Geek,所以我们建模Project和Geek之间关系时设定为@ManyToMany:

@Entity
@Table(name = "T_PROJECT")
public class Project {
	private Long id;
	private String title;
	private List<Geek> geeks = new ArrayList<Geek>();

	@Id
	@GeneratedValue
	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	@Column(name = "TITLE")
	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	@ManyToMany(mappedBy="projects")
	public List<Geek> getGeeks() {
		return geeks;
	}

	public void setGeeks(List<Geek> geeks) {
		this.geeks = geeks;
	}
}

我们的Project有一个内部id、一个String类型的标题和一个Geek的集合。属性geeks的getter方法注解为@ManyToMany(mappedBy=”projects”)。 属性mappedBy的值告诉JPA这个关系的另一端关联的类的成员,因为一个Geek有可能含有多个Project集合。 类Geek如下获取Project集合:

private List<Project> projects = new ArrayList<>();
...
@ManyToMany
@JoinTable(
		name="T_GEEK_PROJECT",
		joinColumns={@JoinColumn(name="GEEK_ID", referencedColumnName="ID")},
		inverseJoinColumns={@JoinColumn(name="PROJECT_ID", referencedColumnName="ID")})
public List<Project> getProjects() {
	return projects;
}

每个ManyToMany关系都需要一个额外的表。这个额外的表需要注解为@JoinTable,其内容在于描述用来存储Geek和不同Project的关联的表的具体信息。此处表名为GEEK_PROJECT,其列GEEK_ID用于存储geek的id,其列PROJECT_ID用于存储project的id。Geek和Project的关联引用列都是ID。

关系@ManyToMany通常也是按照默认方式进行懒加载,因为在大部分情况下,我们不希望在加载某个单独Geek时同时加载它对应的所有Project信息。

由于@ManyToMany关系在两边的设置是对等的,我们需要在两个类中进行对调的对集合引用的注解:

@ManyToMany
@JoinTable(
		name="T_GEEK_PROJECT",
		joinColumns={@JoinColumn(name="PROJECT_ID", referencedColumnName="ID")},
		inverseJoinColumns={@JoinColumn(name="GEEK_ID", referencedColumnName="ID")})
public List<Geek> getGeeks() {
	return geeks;
}

Geek类中的设置:

@ManyToMany(mappedBy="geeks")
public List<Project> getProjects() {
	return projects;
}

这两种情形下Hibernate都会创建一个包含两个列PROJECT_ID和GEEK_ID的新表T_GEEK_PROJECT:

sql> select * from t_geek_project;
PROJECT_ID | GEEK_ID
1          | 2
1          | 4
(2 rows, 2 ms)

用于持久化这些关系的Java代码如下:

List<Geek> resultList = entityManager.createQuery("from Geek g where g.favouriteProgrammingLanguage = :fpl", Geek.class).setParameter("fpl", "Java").getResultList();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
Project project = new Project();
project.setTitle("Java Project");
for (Geek geek : resultList) {
	project.getGeeks().add(geek);
	geek.getProjects().add(project);
}
entityManager.persist(project);
transaction.commit();

在这个例子中,我们只想将那些最喜爱的编程语言是Java的Geek添加到我们的“Java Project”。因此,我们在查询请求中加入一个where子句用来限定只获取列FAV_PROG_LANG含有特定值的那些Geek。由于这个列映射到favouriteProgrammingLanguage字段,我们可以在JPQL声明中直接使用它的Java列名来引用它。在声明中,可以调用setParameter()往JPQL查询(这里是fpl)中动态设值。

5.4. Embedded / ElementCollection

有时你或许会想比你的数据库模型更加精细的构建Java模型。例如,想要建模一个Period类,用来指代在开始和结束日期之间的时间。然后,在每个需要建模Period时间的实体内,都可以重用Period类,这样也就避免了在每个实体内拷贝这两个类字段startDate和endDate。

基于这种情形,JPA提供了嵌入式建模实体的功能。这些实体作为独立的Java类建模,同时注解为@Embeddable:

@Embeddable
public class Period {
	private Date startDate;
	private Date endDate;

	@Column(name ="START_DATE")
	public Date getStartDate() {
		return startDate;
	}

	public void setStartDate(Date startDate) {
		this.startDate = startDate;
	}

	@Column(name ="END_DATE")
	public Date getEndDate() {
		return endDate;
	}

	public void setEndDate(Date endDate) {
		this.endDate = endDate;
	}
}

这个实体可以被Project实体引用:

private Period projectPeriod;

@Embedded
public Period getProjectPeriod() {
	return projectPeriod;
}

public void setProjectPeriod(Period projectPeriod) {
	this.projectPeriod = projectPeriod;
}

引用Peroid实体后,Hibernate会在表T_PROJECT中创建两个列START_DATE和END_DATE:

create table T_PROJECT (
	id bigint generated by default as identity,
	END_DATE timestamp,
	START_DATE timestamp,
	projectType varchar(255),
	TITLE varchar(255),
	primary key (id)
)

虽然这两个字段是在独立的Java类中建模的,我们也可以在查询Project对象时将它们一起查询出来:

sql> select * from t_project;
ID | END_DATE                | START_DATE              | PROJECTTYPE       | TITLE
1  | 2015-02-01 00:00:00.000 | 2016-01-31 23:59:59.999 | TIME_AND_MATERIAL | Java Project
(1 row, 2 ms)

为了在查询Project结果集时限定其指定的开始时间,JPQL需要引用这个嵌入式Peroid类来构建条件子句:

entityManager.createQuery("from Project p where p.projectPeriod.startDate = :startDate", Project.class).setParameter("startDate", createDate(1, 1, 2015));

将会生成如下的SQL查询:

select
	project0_.id as id1_5_,
	project0_.END_DATE as END_DATE2_5_,
	project0_.START_DATE as START_DA3_5_,
	project0_.projectType as projectT4_5_,
	project0_.TITLE as TITLE5_5_ 
from
	T_PROJECT project0_ 
where
	project0_.START_DATE=?

从JPA v2.0开始你甚至可以在一对多关系中使用@Embeddable实体,主要借助于两个新的注解@ElementCollection和@CollectionTable;下面看看Project类中示例代码:

private List<Period> billingPeriods = new ArrayList<Period>();

@ElementCollection
@CollectionTable(
		name="T_BILLING_PERIOD",
		joinColumns=@JoinColumn(name="PROJECT_ID")
)
public List<Period> getBillingPeriods() {
	return billingPeriods;
}

public void setBillingPeriods(List<Period> billingPeriods) {
	this.billingPeriods = billingPeriods;
}

由于Peroid是一个@Embeddable实体,这里我们不能直接使用普通的@OneToMany关系。

6. 数据类型和转换器(Converters)

当处理一些遗留数据库时,有可能发生的情况是:JPA提供的标准映射不能满足我们的需求。下表展示了Java类型是如何映射到不同的数据库类型的:

Java typeDatabase type
String (char, char[])VARCHAR (CHAR, VARCHAR2, CLOB, TEXT)
Number (BigDecimal, BigInteger, Integer, Double, Long, Float, Short, Byte)NUMERIC (NUMBER, INT, LONG, FLOAT, DOUBLE)
int, long, float, double, short, byteNUMERIC (NUMBER, INT, LONG, FLOAT, DOUBLE)
byte[]VARBINARY (BINARY, BLOB)
boolean (Boolean)BOOLEAN (BIT, SMALLINT, INT, NUMBER)
java.util.Date, java.sql.Date, java.sql.Time, java.sql.Timestamp, java.util.CalendarTIMESTAMP (DATE, DATETIME)
java.lang.EnumNUMERIC (VARCHAR, CHAR)
java.util.SerializableVARBINARY (BINARY, BLOB)

上表中很有意思的一点是对enum类型的映射。为了演示JPA中enums的用法,我们为实体增加一个ProjectType enum:

@Entity
@Table(name = "T_PROJECT")
public class Project {
...
	private ProjectType projectType;

	public enum ProjectType {
		FIXED, TIME_AND_MATERIAL
	}
	...
	@Enumerated(EnumType.ORDINAL)
	public ProjectType getProjectType() {
		return projectType;
	}

	public void setProjectType(ProjectType projectType) {
		this.projectType = projectType;
	}
}

从上面的代码段可以看到,注解@Enumerated可以通过指定不同的值和列的映射方式,来达到映射enums和数据库列的目的。设定EnumType.ORDINAL意指映射每个枚举常量到数据库的特定值。当我们设定我们的“Java Project”为TIME_AND_MATERIAL, 会得到如下输出:

sql> select * from t_project;
ID | PROJECTTYPE | TITLE
1  | 1           | Java Project
(1 row, 2 ms)

另一个可选的值是EnumType.STRING。 这样设置的话对应列就是String类型的,可以调用它的name()方法对它进行“编码”:

sql> select * from t_project;
ID | PROJECTTYPE       | TITLE
1  | TIME_AND_MATERIAL | Java Project
(1 row, 2 ms)

如果这两种方案都不能满足你的需求,你也可以编写自己的转换器。这需要保证你的类实现Java接口AttributeConverter,并含有注解@Converter。下面这个示例类演示了转换boolean值为numberic值1或-1:

@Converter
public class BooleanConverter implements AttributeConverter<Boolean, Integer> {

	@Override
	public Integer convertToDatabaseColumn(Boolean aBoolean) {
		if (Boolean.TRUE.equals(aBoolean)) {
			return 1;
		} else {
			return -1;
		}
	}

	@Override
	public Boolean convertToEntityAttribute(Integer value) {
		if (value == null) {
			return Boolean.FALSE;
		} else {
			if (value == 1) {
				return Boolean.TRUE;
			} else {
				return Boolean.FALSE;
			}
		}
	}
}

这个转换类也可以用到IdCard中,用来获取IdCard中代表是否有效的boolean类型的值:

private boolean valid;
...
@Column(name = "VALID")
@Convert(converter = BooleanConverter.class)
public boolean isValid() {
	return valid;
}

public void setValid(boolean valid) {
	this.valid = valid;
}

插入一条属性valid值为false的IdCard记录,会产生如下输出:

sql> select * from t_id_card;
ID | ID_NUMBER | ISSUE_DATE              | VALID
1  | 4711      | 2015-02-04 16:43:30.233 | -1

7. 条件查询(Criteria)API

到现在为止我们已经使用了Java持久化查询语言(JPQL)来执行数据库查询。JPQL的另一种替换方案是“Criteria API”。该API提供了一种基于纯粹Java方法来构建查询的方案。

下面的例子展示了查询数据库中firstName=’Homer’的Person记录:

CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Person> query = builder.createQuery(Person.class);
Root<Person> personRoot = query.from(Person.class);
query.where(builder.equal(personRoot.get("firstName"), "Homer"));
List<Person> resultList = entityManager.createQuery(query).getResultList();

当开始构建条件查询时,我们需要通过EntityManager获得CriteriaBuilder对象。这个builder可以用来创建用来执行真正查询的Query对象。通过调用Query的方法from()指明了需要查询的表,通过参数设置已经映射到对应表的实体。

Query对象还提供了一个方法用来添加where字句:

query.where(builder.equal(personRoot.get("firstName"), "Homer"));

然后通过CriteriaBuilder和它的equal()方法创建这个条件子句。其它更复杂的查询可以通过使用合适的逻辑连接子句完成:

query.where(builder.and(
	builder.equal(personRoot.get("firstName"), "Homer"), 
	builder.equal(personRoot.get("lastName"), "Simpson")));

概括说来CriteriaQuery定义了如下的子句和选项:

  • distinct(): 指明是否过滤数据库中的重复值。
  • from(): 指明当前查询面向的表/实体。
  • select(): 指明一个select查询。
  • multiselect(): 指明一系列查询。
  • where(): 指明查询的where子句。
  • orderBy(): 指明查询结果的排序列。
  • groupBy(): 指明结果集按照什么形式分组。
  • having(): 指明其它附加到结果集上的、限于分组属性的条件。
  • subquery(): 指明其它查询可以使用的子查询。

上述的这些方法可以完全的、动态的、基于用户提供的条件组装出合适的查询语句。

8. 序列(Sequences)

目前为止,我们已经在本教程中使用过注解@GeneratedValue,不过并未设定任何具体信息来指明这个唯一值将会如何分配给每个实体。如果没有指定具体信息的话,JPA提供商会使用它的自有策略来生成这个唯一值。不过我们也可以自己决定以怎样的方式为这些实体生成这些唯一值。因此,JPA提供了如下三种不同的方法:

  • TABLE: 这种策略是让JPA提供商创建一个单独的表,其中为每个实体保存一条记录。这条记录包含实体的名字和id列的当前值;每次有新的id值请求时,就更新此表中相应的行。
  • SEQUENCE: 如果数据库支持序列的话,这个策略可以通过数据库序列获得唯一值。不是所有的数据库产品都支持序列。
  • IDENTITY: 如果数据库支持标识列的话,这个策略就可以使用这种数据库原生支持的列。不是所有的数据库产品都支持标识列。

为了使用TABLE策略,我们需要提供用来做序列管理的表的具体信息给JPA提供商:

@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "TABLE_GENERATOR")
@TableGenerator(name = "TABLE_GENERATOR", table="T_SEQUENCES", pkColumnName = "SEQ_NAME", valueColumnName = "SEQ_VALUE", pkColumnValue = "PHONE")
public Long getId() {
	return id;
}

注解@TableGenerator告诉JPA提供商这里设定的表名为T_SEQUENCES、有两个列SEQ_NAME和SEQ_VALUE。这个表中的这个序列的名字是PHONE:

sql> select * from t_sequences;
SEQ_NAME | SEQ_VALUE
PHONE    | 1

SEQUENCE策略的使用方式是类似的:

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "S_PROJECT")
@SequenceGenerator(name = "S_PROJECT", sequenceName = "S_PROJECT", allocationSize = 100)
public Long getId() {
	return id;
}

使用注解@SequenceGenerator,我们告诉JPA提供商使用到的序列名是S_PROJECT,指定了分配大小(这里是100),即有多少值应预先分配。属性generator和name用来关联这两个注解。

由于这个策略使用的是一个单独的表,当系统中有大量序列值请求时,它很容易成为性能瓶颈。如果你使用同一个序列表服务于很多表,而且该数据库只支持表锁或页锁,尤其会成为性能瓶颈。在这种情况下,数据库会锁定整个表/页,直到当前事务提交完毕。因此JPA支持预定义大小,以使不用频繁请求数据库。

使用IDENTITY策略,只需设置相应的strategy属性:

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() {
	return id;
}

如果当前数据库支持标识列的话,对应的表就会创建如下:

create table T_ID_CARD (
	id bigint generated by default as identity,
	ID_NUMBER varchar(255),
	ISSUE_DATE timestamp,
	VALID integer,
	primary key (id)
)

9. 源码下载

本教程是Java持久化API(JPA)的入门指南。

下载 你可以通过这个链接下载本教程中的所有源代码: jpa_tutorial
Translated by: Vincent Jia
This post is a translation of JPA Tutorial – The ULTIMATE Guide from Martin Mois

Martin Mois

Martin is a Java EE enthusiast and works for an international operating company. He is interested in clean code and the software craftsmanship approach. He also strongly believes in automated testing and continuous integration.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button