概述
JPA 全称 Java Persistence API,与 JDBC 规范类似,同样是 Java EE 规范的一部分,它定义了一套用面向对象的方式操作关系型数据库的接口,它只是一个 ORM 框架的规范,常见的实现包括 Hibernate、TopLink。
实际上 JPA 的发展晚于 Hibernate,EJB 2.0 版本由于实体 bean 过于复杂,很多开发人员使用轻量级的 Hibernate 持久化数据,EJB 3.0 借鉴 Hibernate 将 JPA 作为 EJB 规范的一部分,Hibernate 3.2 版本开始又逐渐实现了 JPA 规范。
sun 公司将 Java EE 捐赠给 Eclipse 基金会后,要求对方不得使用 Java EE 名称,包名也不能再使用 javax,因此 JPA 2.2 版本后来改名 Jakarta Persistence,并在 3.0 版本将 javax.persistence 重命名为 jakarta.persistence,由于包名的修改对现有项目影响很大,因此很多项目目前仍然在使用 JPA 2.2 版本。
关于 Hibernate 和 JPA 的关系,可以用如下的图来表示。
快速上手
先通过一个案例快速认识一下 JPA,这里我们使用的 JPA 实现是 Hibernate,上篇介绍 Hibernate 时主要用 Hibernate 原生的 API 操作数据库,这篇全部以 JPA 的 API 操作数据库,个人认为 JPA 比 Hibernate 原生 API 在使用上还要简单一些。
1. 依赖引入
首先需要引入 JPA 及其实现的依赖,当然了,必要的 JDBC 驱动也是必不可少的,这里使用的是 MySQL 数据库。
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.29</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>5.6.9.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-hikaricp</artifactId> <version>5.6.9.Final</version> </dependency>
除了 MySQL 驱动和 Hibernate,为了使用 HikariCP 数据源还引入了一个 hibernate-hikaricp
依赖。
2. 映射定义
假定数据库中有一个 user
表定义如下。
create table user ( id bigint unsigned auto_increment comment '主键' primary key, username varchar(20) not null comment '用户名', password varchar(20) not null comment '密码', name varchar(20) null comment '姓名', sex varchar(20) null comment '性别', interests varchar(100) null comment '兴趣爱好', version int null comment '版本号', create_by varchar(20) null comment '创建人', create_time datetime null comment '创建时间', update_by varchar(20) null comment '修改人', update_time datetime null comment '修改时间' );
对应的 user
表对应的实体类如下。
@Setter @Getter public class User { private Long id; private String username; private String password; private String name; private SexEnum sex; private List<String> interests; private Integer version; private String createBy; private Date createTime; private String updateBy; private Date updateTime; }
JPA 支持将映射关系定义在 xml 文件中,也支持使用注解定义映射关系,由于注解使用比较方便,这里使用注解表示映射关系,修改实体类如下。
@Setter @Getter @Entity(name = "User") @Table(name = "user") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; private String name; @Enumerated(EnumType.STRING) private SexEnum sex; @Convert(converter = StringListAttributeConverter.class) private List<String> interests; @Version private Integer version; @Column(name = "create_by") private String createBy; @Temporal(TemporalType.TIMESTAMP) @Column(name = "create_time") private Date createTime; @Column(name = "update_by") private String updateBy; @Temporal(TemporalType.TIMESTAMP) @Column(name = "update_time") private Date updateTime; }
3. JPA 配置
按照 JPA 规范的约定, JPA 配置文件应该在类路径 /META-INF/persistence.xml
中,配置内容如下。
<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://java.sun.com/xml/ns/persistence" version="2.0"> <persistence-unit name="default" transaction-type="RESOURCE_LOCAL"> <class>com.zzuhkp.hibernate.entity.User</class> <properties> <property name="javax.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/> <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/test"/> <property name="javax.persistence.jdbc.user" value="root"/> <property name="javax.persistence.jdbc.password" value="12345678"/> <property name="hibernate.hikari.idleTimeout" value="600000"/> <property name="hibernate.hikari.minimumIdle" value="5"/> <property name="hibernate.hikari.maximumPoolSize" value="10"/> <property name="hibernate.show_sql" value="true"/> <property name="hibernate.format_sql" value="true"/> <property name="hibernate.cache.use_second_level_cache" value="false"/> <property name="hibernate.hbm2ddl.auto" value="validate"/> </properties> </persistence-unit> </persistence>
这里每个 persistence-unit 可用于配置一个数据库连接,使用 name 属性为其指定一个名称,并使用 transaction-type 属性指定使用的事务类型,这里我们使用 JDBC 进行事务操作,如果使用 JTA 则将其改为 jta 即可。
class 标签用于指定实体类,需要在实体类上添加表示映射信息的注解。
此外剩余的配置就是 property 了,除了 JPA 中定义的配置项,还可以定义具体 JPA 提供者的配置。
4. 测试代码
这里使用的测试代码如下:
@Slf4j public class Application { public static void main(String[] args) { EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("default"); EntityManager entityManager = entityManagerFactory.createEntityManager(); EntityTransaction transaction = entityManager.getTransaction(); transaction.begin(); try { User user = new User(); user.setUsername("hkp"); user.setPassword("321"); entityManager.persist(user); transaction.commit(); log.info("id:{}", user.getId()); } catch (Exception e) { log.error(e.getMessage()); transaction.rollback(); } finally { entityManager.close(); entityManagerFactory.close(); } } }
JPA 使用方式与 Hibernate 基本保持一致。
核心组件
从上面的代码中可以看到 JPA 有一些关键的组件,包括 EntityManagerFactory、EntityManager、EntityTransaction,它们与 Hibernate 组件的关系可以用如下的图来表示。
Hibernate 的 SessionFactory 接口继承 JPA EntityManagerFactory 接口,Hibernate 的 Session 接口继承 JPA EntityManager 接口,Hibernate 的 Transaction 接口继承 JPA EntiryTransaction 接口。
此外 EntityManagerFactory 和 EntityManager 还定义了一个 unwarp 方法用于获取具体的实现。示例如下。
SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class); Session session = entityManager.unwrap(Session.class);
常用注解
JPA 的很多功能特性都由注解提供元数据,这里总结一些常用的注解。
映射
@Entity
这个注解添加在实体类上,表示这个类是一个实体类,可以使用 name 属性指定实体的名称,实体名称可用于 JPQL 中,默认情况下实体的名称与类名保持一致,如类全限定名为 com.zzuhkp.hibernate.entity.User,则实体的名称为 User。
@Table
这个注解添加在实体类上,指定实体类对应数据库表的元数据,如使用 name 属性指定表名。
@Id
表示主键的注解,加在实体类的字段或 getter 方法上。
@GeneratedValue
加在表示主键的字段或 getter 方法上,指定主键值的生成方式, type 属性值 IDENTITY 表示自增。
@Column
这个注解添加在实体类的字段或 getter 方法上,表示实体类属性对应的数据库表字段,可以使用 name 指定字段名称,如果使用该注解,默认情况下 JPA 认为表字段名称和类属性名称保持一致。
@Temporal
这个注解加在实体类的 Date 或 Calendar 类型的属性上,表示这个属性对应的数据库字段类型,例如是 date、time,还是 timestamp。
@Enumerated
这个注解加在实体类的枚举类型的属性上,使用注解的 value 属性指定数据库表中存储的值。EnumType.STRING 表示将枚举值的名称存至数据库,EnumType.ORDINAL 表示存储枚举值的索引至数据库。
@Version
添加到实体类的属性上,表示该属性为乐观锁字段,支持 int、short、long 及其包装类,以及各种日期类型。JPA 将在 insert 时插入值,update 时将乐观锁字段作为条件。
@Convert
这个注解加在实体类的属性上,用来自定义 Java 类型与数据库类型之间的映射关系。假定 user 表有一个 varchar 类型的 interests 字段记录用户的兴趣爱好,我们希望在 Java 中用一个 List<String> 类型来表示,则可以使用如下的方式自定义映射关系。
@Converter public class StringListAttributeConverter implements AttributeConverter<List<String>, String> { @Override public String convertToDatabaseColumn(List<String> attribute) { if (attribute == null) { return null; } return JSON.toJSONString(attribute); } @Override public List<String> convertToEntityAttribute(String dbData) { if (dbData == null) { return null; } return JSON.parseArray(dbData, String.class); } } public class User { @Convert(converter = StringListAttributeConverter.class) private List<String> interests; }
@Embedded
这个注解加在实体类的属性上,用于对实体类的属性进行分组。假设我们想将 user
表的 name
、sex
字段作为用户基本信息拆到另一个类表示,可以使用如下的方式。
@Getter @Setter @Embeddable public class Basic { private String name; @Enumerated(EnumType.STRING) private SexEnum sex; } public class User { @Embedded private Basic basic; }
关联关系
JPA 提供了一些用于表示实体关联关系的注解,包括 @OneToOne、@OneToMany、@ManyToOne、ManyToMany,支持的集合类型包括 List、Set、Map。
以 @OneToMany 和 @ManyToOne 注解为例,假定一个用户可以有很多收货地址,地址表 address 定义如下。
create table address ( id bigint unsigned auto_increment comment '主键' primary key, user_id bigint unsigned null comment '用户ID', province_name varchar(20) null comment '省份名称', city_name varchar(20) null comment '城市名称', area varchar(20) null comment '区域', detail varchar(100) null comment '详细地址', create_by varchar(20) null comment '创建人', create_time datetime null comment '创建时间', update_by varchar(20) null comment '修改人', update_time datetime null comment '更新时间' );
address
表对应的实体类如下。
@Getter @Setter @Entity @Table(name = "address") public class Address { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "user_id") private User user; @Column(name = "province_name") private String provinceName; @Column(name = "city_name") private String cityName; private String area; private String detail; @Column(name = "create_by") private String createBy; @Temporal(TemporalType.TIMESTAMP) @Column(name = "create_time") private Date createTime; @Column(name = "update_by") private String updateBy; @Temporal(TemporalType.TIMESTAMP) @Column(name = "update_time") private Date updateTime; }
在 User
实体类中可以使用如下的方式表示一对多关联关系。
public class User { @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private List<Address> addresses; }
User 类 addresses 字段上的 @OneToMany 注解表示一个用户有多个地址;
mappedBy 属性的值表示关系被维护的一端,这里的值为 Address 类的 user 字段,表示关系由 Address 类维护;
cascade 属性值表示级联操作,ALL 表示 Address 和 User 状态保持一致。
orphanRemoval 值为 true 表示删除 user 表记录后,同时将 address 记录删除,而不是将 address 的 user_id 值设置为 null。
Address 类 user 字段上的 @JoinColumn 注解则用于描述用于关联的列,这里使用 user_id 字段与 user 表关联。
@MappedSuperclass
这个注解用于添加到实体类的父类上,表示有多个实体类将会继承这个父类。例如,有很多表都有 id、create_by、create_time、update_by、update_time,可以将这几个字段抽象到一个公共的父类中。
@Getter @Setter @MappedSuperclass public class BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "create_by") private String createBy; @Temporal(TemporalType.TIMESTAMP) @Column(name = "create_time") private Date createTime; @Column(name = "update_by") private String updateBy; @Temporal(TemporalType.TIMESTAMP) @Column(name = "update_time") private Date updateTime; }
然后具体的实体类继承这个公共父类即可。
public class User extends BaseEntity { }
回调
JPA 提供了一些用于回调的注解,被这些注解标注的方法成为回调方法,允许用户在 SQL 执行前后附加一些额外的操作。
可以将这些注解直接加在实体的无参无返回值的方法上,例如如果我们想记录操作人操作时间,可以使用如下的方式。
public class BaseEntity { ... 省略字段与 setter/getter 方法 @PrePersist public void setCreateInfo() { this.setCreateBy(UserHolder.getUsername()); this.setCreateTime(new Date()); } @PreUpdate public void setUpdateInfo() { this.setUpdateBy(UserHolder.getUsername()); this.setUpdateTime(new Date()); } }
此外,还可以将回调方法定义在单独的监听器类中,使用方式如下。
public class OperatorListener { @PrePersist public void setCreateInfo(BaseEntity entity) { } @PreUpdate public void setUpdateInfo(BaseEntity entity) { } } @EntityListeners(OperatorListener.class) public class BaseEntity { }
数据库操作
与 Hibernate 一样,JPA 同样提供了四种方式操作数据库。
EntityManager API
JPA EntityManager
的作用与 Hibernate Session
的作用一致,有关 CRUD 的方法定义如下。
public interface EntityManager { public void persist(Object entity); public void remove(Object entity); public void flush(); public <T> T find(Class<T> entityClass, Object primaryKey); }
EntityManager 没有提供 update 方法,不过可以直接对 EntityManager 管理的实体类直接操作,然后手动调用 flush 方法将实体类的修改同步到数据库中。
CriteriaQuery API
EntityManager 每次只能直接操作数据库单条记录对应的某一个实体类,功能上相对受限一些。为了应对复杂的查询操作,EntityManager 还提供了一个 CriteriaQuery 接口以 Java API 的方式查询数据库。
以登录场景为例,根据用户名和密码查询用户的示例如下。
public User getUser(String username, String password) { CriteriaBuilder build = entityManager.getCriteriaBuilder(); CriteriaQuery<User> criteriaQuery = build.createQuery(User.class); Root<User> root = criteriaQuery.from(User.class); criteriaQuery.select(root); criteriaQuery.where(build.and( build.equal(root.get("username"), username), build.equal(root.get("password"), password))); User user = entityManager.createQuery(criteriaQuery).getSingleResult(); return user; }
上面的代码翻译成 SQL 可以简单理解如下:
select * from user where username = ? and password = ?
可以看到,即便是一个比较简单的查询也用了不少代码来描述,更别提一些复杂的场景了,因此 CriteriaQuery 在实际项目中使用并太多,这里也不再对上面的代码进行解释。
JPQL
除了 CriteriaQuery,JPA 还借鉴 SQL 提出了一种名为 JPQL 的查询语言,它以对象为中心,语法与 SQL 大同小异,查询时把表名改为实体名即可。还是以上面登录的场景为例,用 JPQL 查询用户的方式如下。
public User getUser(String username, String password) { User user = entityManager.createQuery( "select u from User as u where username = :username and password = :password", User.class) .setParameter("username", username).setParameter("password", password) .getSingleResult(); return user; }
这种方式确实比 CriteriaQuery
简单了许多,如果不想使用 SQL 的话使用 JPQL 处理复杂场景是一个比较好的选择。
此外,如果有可以复用的 JPQL,还可以将其定义在实体上,示例如下。
@NamedQuery(name = "login", query = "select u from User as u where username = :username and password = :password") public class User extends BaseEntity { } public User getUser(String username, String password) { User user = entityManager.createNamedQuery("login", User.class) .setParameter("username", username).setParameter("password", password) .getSingleResult(); return user; }
原生 SQL
JPA 支持使用原生 SQL 操作数据库,如果想要使用特定于数据库的功能,例如函数,这是个比较好的选择,使用 SQL 实现登录场景的用户查询方式如下。
public User getUser(String username, String password) { User user = (User) entityManager.createNativeQuery( "select * from user where username = :username and password = :password", User.class) .setParameter("username", username).setParameter("password", password) .getSingleResult(); return user; }
可以看到 SQL 与 JPQL 的语法确实比较相似。
总结
JPA 作为 Java 中的 ORM 框架规范,虽然提供了众多特性,但都与映射或数据库操作有关,相对 Hibernate 简单一些,Spring 框架也对 JPA 与 Hibernate 进行了支持,后续将进行总结。