伴随着 Java 诞生与发展,目前 Java 界涌现出了五花八门的数据访问技术,有一些名词甚至达到了耳熟能详的程度,包括 JDBC、JTA、JPA、ORM、MyBatis 等等,这篇介绍的是 Spring Data 项目中的 spring-data-jpa 框架,了解其他相关技术可以查阅我前面的文章。
由于这个框架目前在国内使用较少,本人也没有在实际项目中使用过,因此本篇更多的是带领大家对它有一个基本的认识,至于源码部分这里也不再进行分析,有感兴趣的小伙伴可以留言讨论交流。
在 Java 中,一门技术的诞生往往是规范先行。为了在 Java 中以统一的方式访问不同的关系型数据库,sun 公司制定了 JDBC 规范,由各数据库厂商提供具体的实现。
JDBC 定义了对单个数据库的事务的使用方式,如果有多个数据库想同时加入事务,JDBC 就力不从心了,因此有时候我们还会听到 JTA 的声音,JTA 通过两阶段提交支持多个数据库加入一个事务。
由于关系型数据库和面向对象编程的不同,加之 JDBC 规范的复杂性,在实际使用中会出现大量的样板式代码,包括创建连接、创建语句、查询数据库、操作结果转换为 Java 对象、关闭结果集、关闭语句、关闭连接。
为了应对使用 JDBC 的复杂性,诞生出了不少 ORM 框架,ORM 框架将这些通用的操作封装在内部,而将必须由用户定义的部分暴露出来,例如映射关系、一些复杂的 SQL 等等。
比较热门的一个 ORM 框架是 Hibernate,仅仅定义映射关系就足够了,Hibernate 将 JDBC 封装在内部,可以只使用框架提供的 API 操作数据库,一行 SQL 都不用手工书写。
另一种比较热门的 ORM 框架是 MyBatis,由于每个查询都需要单独提供映射关系,MyBatis 也被称为半自动化 ORM 框架,MyBatis 的 SQL 是手动定义的,因此相对 Hibernate 更灵活一些,能适用更复杂的场景,目前比较流行。
由于不同的 ORM 框架使用方法有所不同,为了统一 ORM 框架的使用,sun 公司又设计了一套 ORM 规范,这就是我们常听到的 JPA。JPA 的设计参考了 Hibernate,Hibernate 后来又反向实现了 JPA 规范,Hibernate 也成了目前最常用的 JPA 实现。
Spring 框架作为 Java 事实上的标准,也对 JPA 进行了整合,最初在 spring-framework 框架中的 spring-orm 模块进行了整合,不过这里只是将 JPA 加入到 Spring 的事务管理中。
除了 spring-orm,Spring 对 JPA 整合的另一个模块是 spring-data-jpa,也就是今天的主题,关于上面提到的这些技术,可以用如下的图示来表示。

spring-data-jpa 其实只是 Spring Data 项目中的其中一个模块,Spring Data 项目旨在以相同或相似的方式操作不同的持久化实现,包括各种关系型数据库、非关系型数据库等。
其中 spring-data-commons 是其他模块依赖的公共模块,保证了不同持久化实现的使用方式统一,Spring Data 各模块通用的使用方式可参考《Spring 加强版 ORM 框架 Spring Data 入门》,这篇主要介绍 spring-data-jpa 特有的一些使用方式。
由于 spring-orm 已经实现了 JPA 与 Spring 事务的整合,spring-data-jpa 在底层直接复用了 spring-orm,只是在使用方式上又包装了一层,以适配 Spring Data 项目。如果你对 spring-orm 不了解,可以参考 《Spring 项目快速整合 JPA》,本篇同样参照了其中的示例。
下面通过案例的形式演示如何在项目中使用 spring-data-jpa。
使用 spring-data-jpa 首先需要引入依赖,先来看下在 spring-framework 项目中的使用方式。
由于 Spring Data 项目各模块的发布时间有所不同,各模块以及底层依赖的 spring-framework 项目模块的版本号无法做到统一,Spring Data 官方提供了一个 bom 来维护版本号,将其添加到 maven pom 文件,然后就可以省略 Spring Data 各模块的版本号了。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.datagroupId>
<artifactId>spring-data-bomartifactId>
<version>2021.0.7version>
<scope>importscope>
<type>pomtype>
dependency>
dependencies>
dependencyManagement>
除了这个 bom ,我们还需要添加其他的依赖,具体如下。
<dependency>
<groupId>org.springframework.datagroupId>
<artifactId>spring-data-jpaartifactId>
dependency>
<dependency>
<groupId>org.hibernategroupId>
<artifactId>hibernate-coreartifactId>
<version>5.6.9.Finalversion>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.29version>
dependency>
<dependency>
<groupId>com.zaxxergroupId>
<artifactId>HikariCPartifactId>
<version>4.0.3version>
dependency>
其中 spring-data-jpa 无需指定版本号,Hibernate 作为 JPA 的实现,除此之外还需要引入具体数据库的驱动,这里使用的是 MySQL,然后还需要引入获取 Connection 的数据源,这里使用的是 HikariCP。
测试使用的数据库表如下。
create table user
(
id bigint unsigned auto_increment comment '主键'
primary key,
username varchar(20) not null comment '用户名',
password varchar(20) not null comment '密码'
)
映射关系我们选择使用在实体类上添加注解配置。
@Getter
@Setter
@Entity(name = "User")
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
}
每个 ORM 框架都有一个操作数据库的核心类,例如对于 MyBatis 来说是 SqlSession,对于 Hibernate 来说是 Session,对于 JPA 来说是 EntityManager,对于 Spring Data 来说则是 Repository。
不过 Spring Data 的 Repository 比较特殊,它是一个由用户定义的接口,用户可以提供自定义的实现,也可以由 Spring Data 具体模块创建接口的代理作为 bean,通过解析方法来查找具体实现。
为了创建 Repository 接口的实现 bean,可以通过 @EnableJpaRepository 注解来开启,示例代码如下。
@Configuration
@EnableJpaRepositories(basePackages = "com.zzuhkp.jpa",
entityManagerFactoryRef = "entityManagerFactory",
transactionManagerRef = "transactionManager")
@EnableTransactionManagement
public class JpaConfig {
@Bean
public DataSource dataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/test");
dataSource.setDriverClassName(Driver.class.getName());
dataSource.setUsername("root");
dataSource.setPassword("12345678");
return dataSource;
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
Properties jpaProperties = new Properties();
jpaProperties.put(AvailableSettings.SHOW_SQL, true);
jpaProperties.put(AvailableSettings.FORMAT_SQL, true);
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
entityManagerFactoryBean.setDataSource(dataSource());
entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
entityManagerFactoryBean.setJpaProperties(jpaProperties);
entityManagerFactoryBean.setPackagesToScan("com.zzuhkp.jpa.entity");
return entityManagerFactoryBean;
}
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory);
transactionManager.setDataSource(dataSource());
return transactionManager;
}
}
spring-data-jpa 依赖 spring-orm,因此有一些配置是和在 spring-orm 中使用 JPA 相同的,包括数据源、EntityManagerFactory、事务管理器。
@EnableJpaRepositories 注解的 basePackages 属性可以指定为哪些包中的 Repository 创建代理 bean。entityManagerFactoryRef 可以指定底层依赖的 EntityManagerFactory,默认值为 entityManagerFactory。transactionManagerRef 可以指定底层依赖的事务管理器,默认值为 transactionManager。数据操作最重要的是 Repository 的定义,我们一般会定义一个继承 spring-data-jpa 模块提供的 JpaRepository 接口的 Repository 接口,这种方式可以大大简化一些常用操作方法的重复定义,这有些类似 MyBatis-Plus 框架提供的 BaseMapper。示例如下。
public interface UserRepository extends JpaRepository<User, Long> {
}
根据前面 @EnableJpaRepository 注解的配置,spring-data-jpa 会自动为这个接口提供实现并注册为 bean。
对于单表的一些简单操作,使用 JpaRepository 接口提供的方法就可以了,JpaRepository 还继承了一些其他接口,使用非常方便,接口定义如下。
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
}
PagingAndSortingRepository 是所有 Spring Data 模块都支持的接口,提供了基本的增删改查方法,QueryByExampleExecutor 是 spring-data-jpa 模块特有的接口,用于复杂查询,先看下 JpaRepository 提供的一些方法。
1. 添加/修改
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
<S extends T> List<S> saveAll(Iterable<S> entities);
<S extends T> S saveAndFlush(S entity);
<S extends T> List<S> saveAllAndFlush(Iterable<S> entities);
}
与其他 Spring Data 模块一样,spring-data-jpa 对于添加和修改操作,使用的是相同的方法,根据是否为新记录决定进行何种操作。
默认情况先根据版本号字段和标识符字段判断是否为新实体,如果实体中的标识符字段值是手动设置的,可以选择将实体实现接口 Persistable 自定义判断逻辑。
2. 删除
JpaRepository 包含的删除方法如下。
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
void deleteAllInBatch(Iterable<T> entities);
void deleteAllByIdInBatch(Iterable<ID> ids);
void deleteAllInBatch();
}
3. 查询
JpaRepository 包含的简单查询方法如下。
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
List<T> findAll();
List<T> findAll(Sort sort);
List<T> findAllById(Iterable<ID> ids);
}
Example 能解决一些比 CrudRepository 更复杂的问题,主要用于属性匹配,需要 Repository 继承接口 QueryByExampleExecutor 才可以,示例如下。
public User login(String username, String password) {
User user = new User();
user.setUsername(username);
user.setPassword(password);
Optional<User> one = userRepository.findOne(Example.of(user));
return one.get();
}
等价于如下 SQL:
select id,username,password from user where username = ? and password = ?
spring-data-jpa 提供了对 JPA CriteriaQuery 的支持,将其封装在了 Specification 接口内部,通过 Repository 继承接口 JpaSpecificationExecutor 就可以使用了,示例如下。
public User login(String username, String password) {
Optional<User> one = userRepository.findOne(new Specification<User>() {
@Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
return criteriaBuilder.equal(root.get("username"), username);
}
}.and(new Specification<User>() {
@Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
return criteriaBuilder.equal(root.get("password"), password);
}
}));
return one.get();
}
上面的查询等价于如下的 SQL:
select * from user where username = ? and password = ?
可以看到,这种使用方式实在太复杂了,强烈不建议使用。
对于一些复杂方法的查询,主要依靠在 Repository 自定义查询方法实现的,那么 spring-data-jpa 是怎样将方法签名解析为具体的 SQL 执行的呢?
可以通过 @EnableJpaRepository 注解的 queryLookupStrategy 属性配置,各取值含义如下。
| 取值 | 含义 |
|---|---|
| CREATE | 从方法名中解析 SQL |
| USE_DECLARED_QUERY | 从声明的查询中解析 SQL |
| CREATE_IF_NOT_FOUND | 优先从声明的查询中解析 SQL,如果解析不到则使用方法名解析,这个是默认的查找策略 |
根据方法名解析 SQL 需要方法名遵循特定的结构,以用户登录查询用户信息为例,示例如下。
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsernameAndPassword(String username, String password);
}
这将转换为如下的 SQL:
select id,username,password from user where username = ? and password = ?
如果你使用的 IDE 是 idea,还会有非常智能的代码提示。

更多查询关键字,可以参考 spring-data-jpa 官网 附录 C:存储库查询关键字 一节。
1. 命名查询
spring-data-jpa 对 JPA 提供的命名查询提供了支持,可以将命名查询的注解用在实体类上。
@NamedQuery(name = "User.login", query = "select u from User u where u.username = ?1 and u.password = ?2")
public class User {
}
然后在 Repository 中将命名查询的名字作为方法名即可。
public interface UserRepository extends JpaRepository<User, Long> {
User login(String username, String password);
}
2. @Query 查询
spring-data-jpa 还提供了一个 Query 注解,同时支持 JPQL 和 SQL,命名查询和非命名查询,将这个注解添加在 Repository 方法上即可,与上述 @NamedQuery 等价的 @Query 注解如下。
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User where u.username = ?1 and u.password = ?2")
User login(String username, String password);
}
如果想使用 SQL 查询,可以修改如下。
public interface UserRepository extends JpaRepository<User, Long> {
@Query(value = "select * from user where username = ?1 and password = ?2",
nativeQuery = true)
User login(String username, String password);
}
如果想使用命名查询,可以将 JPQL 或 SQL 配置在 META-INF/jpa-named-queries.properties 文件中。
User.login = select u from User u where u.username = ?1 and u.password = ?2
然后在 @Query 注解中指定名称即可。
public interface UserRepository extends JpaRepository<User, Long> {
@Query(name = "User.login")
User login(String username, String password);
}
另外这个命名查询文件的位置还可以在 @EnableJpaRepository 的 namedQueriesLocation 属性中配置。
如果需要分页或者排序,可以将 Pageable 或 Sort 参数附加在方法参数后面,同时支持 @NamedQuery 与 @Query 注解,示例如下。
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.username = ?1")
Page<User> page(String username, Pageable pageable);
@Query("select u from User u where u.username = ?1")
List<User> sort(String username, Sort sort);
}
默认情况,spring-data-jpa 采用基于位置的参数绑定,在重构代码时很容易出错,为了解决这个问题,spring-data-jpa 添加了对命名参数的支持,使用示例如下。
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.username = :user and u.password = :pwd")
User login(@Param("user") String username, @Param("pwd") String password);
}
JPQL 或 SQL 中的参数使用 :参数名 的形式表示,然后在方法参数前使用 @Param 指定对应的 JPQL 或 SQL 参数。
spring-data-jpa 同样支持复杂的修改 SQL,在方法上添加 @Modifying 注解即可,示例如下。
public interface UserRepository extends JpaRepository<User, Long> {
@Modifying
@Query("delete from User u where u.username = :user and u.password = :pwd")
int delete(@Param("user") String username, @Param("pwd") String password);
}
spring-data-jpa 默认支持 Spring 的事务,对于查询操作 readyOnly 将设置为 true,也可以在 Repository 方法上手动修改事务配置。
public interface UserRepository extends JpaRepository<User, Long> {
@Transactional(timeout = 20)
User findByUsernameAndPassword(String username, String password);
}
审计用于记录创建或修改的人以及时间,spring-data-jpa 提供了独有的支持。
public class User {
@CreatedDate
private Date createTime;
@CreatedBy
private String createBy;
@LastModifiedDate
private Date updateTime;
@LastModifiedBy
private String updateBy;
}
在实体类上添加上面的注解即可,spring-data-jpa 可以取当前时间作为创建或修改时间,对于操作人则需要手动告诉 spring-data-jpa。
@EnableJpaAuditing
public class JpaConfig {
@Bean
public AuditorAware<String> auditorAware() {
return new AuditorAware<String>() {
@Override
public Optional<String> getCurrentAuditor() {
return Optional.of("currentUser");
}
};
}
}
AuditorAware bean 用来指定当前操作人,@EnableJpaAuditing 注解则用于开启审计功能。
如果不想使用注解,还可以将实体类实现 Auditable 接口,这里不再赘述。
spring-boot 对 spring-data-jpa 的支持主要在于自动化配置,添加一个 starter 即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
这个 starter 包含了 Hibernate、HikariCP 的依赖,Spring 会自动配置 DataSource、EntityManagerFactory、TransactionManager,以及启用 JPA Repository bean 注册与查找的功能,当然了,必须的数据库驱动还是需要用户手动提供的。
spring-data-jpa 对 JPA 进行了封装与简化,如果需要使用 JPA,建议直接引入 spring-boot-starter-data-jpa。