JPA(Java Persistence API)是JavaEE中的持久化规范,JPA规范定义了标准JavaEE应用程序中,数据实体和关系型数据库表之间如何映射,以及JavaEE应用程序如何操作数据库表。JPA不是一个ORM框架而是一套ORM的规范接口,Hibernate、Oracle TopLink、Apache OpenJPA等才是具体的ORM实现框架,但JPA规范的存在能让我们无缝的切换这些ORM框架,而不用关心绝大多数的底层配置。
本系列笔记我们将以Wildfly14、MySQL5.7数据库和JavaEE8环境为例介绍JPA的用法。注意我们这里使用的是传统JavaEE应用服务器环境,对于Spring中如何使用JPA请参考Spring相关章节。
JPA规范是在JavaEE5中首次发布的。在早期的EJB2.0时代,操作数据库需要使用EJB规范的实体Bean,但随着时代的发展,实体Bean这个概念在EJB3.0中被废弃了,数据库操作则单独抽取出了JPA规范,这让数据库ORM能够脱离EJB,JavaEE程序的耦合性进一步降低,EJB3.0也变得更加轻量级了。
其实JPA不仅能在JavaEE环境中使用,也可以在JavaSE中使用,我们只需要引入JPA接口和任意的实现框架就可以在任何Java程序中使用JPA。另一点要说明的是JPA和基本没人用的EJB不同,JPA至今也还是一个相当成功的JavaEE规范,Spring生态中也对JPA进行了封装实现了SpringDataJPA
项目,它也是Spring中非常流行的数据库ORM操作方式。我们实际开发中无论使用标准JavaEE技术栈还是Spring技术栈,如果要使用ORM也都应该优先考虑JPA,而不是直接使用Hibernate等具体的ORM框架的API,这样我们的程序会具有更好的可移植性。
在Wildfly14中,JPA的默认实现是Hibernate,当然正如前面所说,我们其实不必太关心JPA的实现框架,因为我们使用的基本都是JPA规范,底层实现对我们来说是透明的。如果你对Hibernate十分了解,使用JPA并不意味着Hibernate的知识就“白学了”,相反你甚至可以直接上手使用JPA,因为JPA的用法和Hibernate非常相似,很多API和基础概念基本就是改了下名字。
在JavaEE环境下,使用JPA前我们需要先在JavaEE应用服务器内配置数据源,这里以Wildfly14应用服务器和MySQL数据库为例进行介绍。
首先我们需要获取MySQL的JDBC驱动程序Jar包,这个Jar包可以从Maven中央仓库手动下载获得,我这里使用的是mysql-connector-java-8.0.27.jar
。我们需要将这个Jar包放置在modules/system/layers/base/com/mysql/main
目录下。
modules/system/layers/base/com/mysql/main
|_ mysql-connector-java-8.0.27.jar
|_ module.xml
此外,我们还需要一个Wildfly的模块描述文件module.xml
,它的内容如下。这个描述文件中配置了模块名、Jar包名、依赖等信息。Wildfly应用服务器中有一个默认的测试用h2数据库的数据源,如果不会写,可以参考旁边的h2数据库描述文件编写。
<?xml version="1.0" encoding="UTF-8"?>
<module name="com.mysql" xmlns="urn:jboss:module:1.5">
<resources>
<resource-root path="mysql-connector-java-8.0.27.jar"/>
</resources>
<dependencies>
<module name="javax.api"/>
<module name="javax.transaction.api"/>
<module name="javax.servlet.api" optional="true"/>
</dependencies>
</module>
配置好模块后,我们还需要编辑Wildfly的主配置文件,在启动时加载模块和数据源。我们这里使用的配置文件是standalone-full
,因此编辑standalone/configuration/standalone-full.xml
文件。在<subsystem xmlns="urn:jboss:domain:datasources:5.0">
下我们添加数据源配置和驱动配置,原来默认有一个测试用的h2数据源,我们新增MySQL相关的配置。
<subsystem xmlns="urn:jboss:domain:datasources:5.0">
<datasources>
<datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true">
<connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</connection-url>
<driver>h2</driver>
<security>
<user-name>sa</user-name>
<password>sa</password>
</security>
</datasource>
<datasource jndi-name="java:jboss/datasources/NetstoreDS" pool-name="NetstoreDS" enabled="true" use-java-context="true">
<connection-url>jdbc:mysql://localhost:3306/netstore?useSSL=false&serverTimezone=Asia/Shanghai</connection-url>
<driver>mysql</driver>
<security>
<user-name>root</user-name>
<password>root</password>
</security>
<validation>
<valid-connection-checker class-name="org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLValidConnectionChecker"/>
<background-validation>true</background-validation>
<exception-sorter class-name="org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLExceptionSorter"/>
</validation>
</datasource>
<drivers>
<driver name="h2" module="com.h2database.h2">
<xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
</driver>
<driver name="mysql" module="com.mysql">
<xa-datasource-class>com.mysql.cj.jdbc.MysqlXADataSource</xa-datasource-class>
</driver>
</drivers>
</datasources>
</subsystem>
数据源配置完成后,我们可以在Wildfly14的管理控制台中看到我们配置的数据源信息。
我们这里直接以一个例子的形式介绍如何在标准JavaEE工程中使用JPA。我们创建一个EJB工程,工程目录结构如下。
netstore-ejb
|_ src/main/java # 源码目录
|_ src/main/resources # 资源目录
|_ META-INF
|_ persistence.xml # JPA配置文件
工程中我们需要先配置persistence.xml
,它是JPA的核心配置文件,其中定义了工程中使用的持久化单元(Persistence Unit),配置字段包括数据源JNDI、事务类型等信息,此外针对不同的实现ORM框架,这里还可以传入一些具体配置,这里也是使用JPA时唯一涉及配置具体ORM实现框架的地方。
persistence.xml
<persistence version="2.2" xmlns="http://xmlns.jcp.org/xml/ns/persistence">
<persistence-unit name="jpa-netstorer-pu" transaction-type="JTA">
<jta-data-source>java:jboss/datasources/NetstoreDS</jta-data-source>
<properties>
<property name="hibernate.hbm2ddl.auto" value="none"/>
<property name="hibernate.show_sql" value="true"/>
</properties>
</persistence-unit>
</persistence>
hibernate.hbm2ddl.auto
:Entity发生变更时是否自动更新数据库中的表结构,大型JavaEE项目中通常采用Database First开发模式(也就是先设计数据库表再自动生成或手动编写实体类),因此设置为none
hibernate.show_sql
:打印SQL执行日志Hibernate其实还有很多其它配置,例如二级缓存、批量操作等,我们这里对于Hibernate的配置不再展开介绍,具体可以参考Java/Java企业级应用框架/Hibernate
相关章节。
Student.java
是一个对应于数据库表的实体类,JPA中这种类被称为Entity,一般来说实体类对应于一个数据库表,它的每个字段对应于数据库表的一列。实体类上会标注许多JPA注解,这些配置注解决定了实体类和数据库表之间的映射关系。实体类使用普通Java类即可,但对其有一些要求,首先实体类需要有public
的无参构造函数,因为JPA操作数据时需要调用无参构造函数;其次对于标注了JPA注解的字段还需要提供Get/Set方法;此外还需要重写equals()
和hashCode()
方法,这是因为有时我们需要将实体类放入Set集合,这可能涉及实体类对象的相等判断操作。
Student.java
package com.gacfox.netstore.ejb.entity;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.Objects;
@Entity
@Table(name = "t_student")
public class Student implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "name")
private String name;
@Column(name = "age")
private Integer age;
@Column(name = "create_time")
private Date createTime;
public Student() {
}
public Student(Long id, String name, Integer age, Date createTime) {
this.id = id;
this.name = name;
this.age = age;
this.createTime = createTime;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", createTime=" + createTime +
'}';
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(id, student.id) && Objects.equals(name, student.name) && Objects.equals(age, student.age) && Objects.equals(createTime, student.createTime);
}
@Override
public int hashCode() {
return Objects.hash(id, name, age, createTime);
}
}
最后是我们的EJB类,它是一个无状态会话Bean,该类提供了一个createStudent()
方法,能够根据传入参数创建学生实体对象并持久化到数据库中。
package com.gacfox.netstore.api;
import com.gacfox.netstore.api.model.StudentDto;
import javax.ejb.Local;
@Local
public interface StudentService {
void createStudent(StudentDto studentDto);
}
package com.gacfox.netstore.ejb;
import com.gacfox.netstore.api.StudentService;
import com.gacfox.netstore.api.model.StudentDto;
import com.gacfox.netstore.ejb.entity.Student;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
@Stateless
public class StudentServiceImpl implements StudentService {
@PersistenceContext(unitName = "jpa-netstorer-pu")
private EntityManager entityManager;
@Override
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void createStudent(StudentDto studentDto) {
Student student = new Student();
student.setId(studentDto.getId());
student.setName(studentDto.getName());
student.setAge(studentDto.getAge());
student.setCreateTime(studentDto.getCreateTime());
entityManager.persist(student);
}
}
JPA的所有操作都依赖于EntityManager
,EJB中我们需要注入这个对象,JPA提供了@PersistenceContext
注解来注入EntityManager
,它的参数unitName
需要指定我们在之前配置文件中配置的持久化单元名。
createStudent()
方法上我们标注了@TransactionAttribute(TransactionAttributeType.REQUIRED)
,表示该方法需要在数据库事务中运行,如果没有事务则会新创建事务。这里的事务其实默认就是REQUIRED
的,因此这行代码其实也可以省略。方法体中,我们实例化了Student
这个Entity对象,并调用EntityManager
将数据保存到了数据库。