JPA(Java Persistence API)是JavaEE标准中,一个类似Hibernate的持久化API,实际上和Hibernate使用起来没什么区别,只不过JPA是“JavaEE标准的”。Hibernate(JPA)虽然配置略微麻烦,但是使用起来还是比较方便的,Java类和数据库表映射配置完成后,我们写很少的HQL(JPA中是JPQL)代码就能完成比较复杂的业务功能。
然而,Spring Data JPA能够让我们几乎不写任何SQL(HQL、JPQL)就能完成相同的查询功能,尤其是配合SpringBoot使用,开发效率更是相当不错。下面简单介绍一下如何配置和使用Spring Data JPA,例子中并没有使用Web工程,只是创建了一个简单的Java工程并结合使用了Spring框架,但是原理是一样的。
提醒:
如果在单独的工程中使用Spring Data JPA需要引入如下依赖:
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>2.0.9.RELEASE</version>
</dependency>
<!-- Hibernate作为JPA实现 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>4.3.11.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.3.11.Final</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
<!-- Hikari连接池 -->
<dependency>
<groupId>hikari-cp</groupId>
<artifactId>hikari-cp</artifactId>
<version>2.6.0</version>
</dependency>
如果使用SpringBoot环境就简单多了,直接引入起步依赖和对应数据库驱动即可:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
如果不使用SpringBoot,我们需要手动配置数据源、JPA实体管理器、事务管理器等,这些对象配置起来比较复杂,我们这里直接给出一个完整的例子:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<context:component-scan base-package="com.gacfox.demojpa" />
<!-- Spring Data JPA Repo对象配置 -->
<jpa:repositories base-package="com.gacfox.demojpa.dao"
repository-impl-postfix="Impl"
entity-manager-factory-ref="entityManagerFactory"
transaction-manager-ref="txManager" />
<!-- 配置Hikari数据源 -->
<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
<property name="poolName" value="mercatus_connection_pool" />
<property name="dataSourceClassName" value="com.mysql.cj.jdbc.MysqlDataSource" />
<property name="maximumPoolSize" value="50" />
<property name="maxLifetime" value="60000" />
<property name="idleTimeout" value="30000" />
<property name="dataSourceProperties">
<props>
<prop key="url"><![CDATA[jdbc:mysql://127.0.0.1:3306/jpa_demo?useSSL=false&serverTimezone=GMT%2B8]]></prop>
<prop key="user">root</prop>
<prop key="password">root</prop>
<prop key="prepStmtCacheSize">250</prop>
<prop key="prepStmtCacheSqlLimit">2048</prop>
<prop key="cachePrepStmts">true</prop>
<prop key="useServerPrepStmts">true</prop>
</props>
</property>
</bean>
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
<constructor-arg ref="hikariConfig" />
</bean>
<!-- JPA实体管理器 -->
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="packagesToScan" value="com.gacfox.demojpa.model" />
<property name="persistenceProvider">
<bean class="org.hibernate.ejb.HibernatePersistence" />
</property>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="generateDdl" value="false" />
<property name="database" value="MYSQL" />
<property name="databasePlatform" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
<property name="showSql" value="true" />
</bean>
</property>
<property name="jpaDialect">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect" />
</property>
<property name="jpaPropertyMap">
<map>
<entry key="hibernate.query.substitutions" value="true 1, false 0" />
<entry key="hibernate.default_batch_fetch_size" value="16" />
<entry key="hibernate.max_fetch_depth" value="2" />
<entry key="hibernate.generate_statistics" value="true" />
<entry key="hibernate.bytecode.use_reflection_optimizer" value="true" />
<entry key="hibernate.cache.use_second_level_cache" value="false" />
<entry key="hibernate.cache.use_query_cache" value="false" />
</map>
</property>
</bean>
<!-- 事务管理器 -->
<bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
<!-- 注解驱动的事务支持 -->
<tx:annotation-driven transaction-manager="txManager" />
</beans>
SpringBoot环境下,配置都集成在application.properties
项目配置文件中。许多配置在SpringBoot下都是约定的默认值,下面是一些例子:
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/jpa_demo?useSSL=false&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.database=MYSQL
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
这里实际上还有很多其他可选项,具体可以参考文档。
下面我们编写一个例子,通过Spring Data JPA实现最简单的增删改查。
下面所有的例子都基于以下三个数据表:
create table t_user (
user_id bigint auto_increment,
username varchar(20),
password varchar(20),
birthday datetime,
primary key (user_id)
);
create table t_role (
role_id bigint auto_increment,
role_name varchar(20),
primary key (role_id)
);
create table t_user_role (
user_role_id bigint auto_increment,
user_id bigint,
role_id bigint,
primary key (user_role_id)
);
其中用户表和角色表具有多对多关系,多对多关系通过一张中间表来表达。Java类和数据库表的映射关系这里使用JPA注解进行配置(有关JPA注解的内容在Hibernate章节都介绍过了)。
User.java
package com.gacfox.demojpa.model;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.Set;
@Entity
@Table(name = "t_user")
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long userId;
@Column(name = "username")
private String username;
@Column(name = "password")
private String password;
@Column(name = "birthday")
private Date birthday;
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinTable(name = "t_user_role", joinColumns = { @JoinColumn(name = "user_id") }, inverseJoinColumns = {
@JoinColumn(name = "role_id") })
private Set<Role> roleSet;
// ... 构造函数、GET/SET、toString等方法,为了节省篇幅此处省略
}
Role.java
package com.gacfox.demojpa.model;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Set;
@Entity
@Table(name = "t_role")
public class Role implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "role_id")
private Long roleId;
@Column(name = "role_name")
private String roleName;
@ManyToMany(mappedBy = "roleSet", fetch = FetchType.LAZY)
private Set<User> userSet;
// ... 构造函数、GET/SET、toString等方法,为了节省篇幅此处省略
}
下面代码定义了一个接口,实现了一些常用的Dao功能。
UserRepository.java
package com.gacfox.demojpa.dao;
import org.springframework.data.jpa.repository.JpaRepository;
import com.gacfox.demojpa.model.User;
import java.util.List;
public interface UserRepository extends JpaRepository<User, Long> {
/**
* 根据用户ID查询用户
*
* @param userId 用户ID
* @return 用户对象
*/
public User findByUserId(Long userId);
/**
* 根据用户名查询用户
*
* @param username 用户名
* @return 用户对象
*/
public User findByUsername(String username);
/**
* 根据用户名和密码查询用户
*
* @param username 用户名
* @param password 密码
* @return 用户对象
*/
public User findByUsernameAndPassword(String username, String password);
/**
* 模糊查询用户
*
* @param pattern 匹配字符串,如[prefix]%这种形式
* @return 用户对象列表
*/
public List<User> findByUsernameLike(String pattern);
}
注意代码中UserRepository
这个类的定义,它继承了JpaRepository
,我们不需要写任何实现类,Spring Data JPA已经为我们做好了,我们只需要在我们的Service层中调用UserRepository
即可。
这看起来比较神奇,但实际上Spring Data JPA会通过我们定义的方法名自动生成数据库查询,规则是这样的:
findBy
开头findBy
后面,名字需要和Java实体类的字段名相对应,如findByUsername
And
或Or
等连接,如findByUsernameAndPassword
Like
,如findByUsernameLike
上述规则能够覆盖80%的查询操作了,这就是Spring Data JPA的方便之处。
当然,如果查询条件十分复杂,上面这种写法不能满足的时候,我们也可以写接口的实现类,那就和Hibernate(JPA)没什么区别了,下面例子中我们自己实现了一个接口用于查询。
UserRepository.java
public Role findByRoleName2(String roleName);
package com.gacfox.demojpa.dao;
import com.gacfox.demojpa.model.Role;
import org.springframework.beans.factory.annotation.Autowired;
import javax.persistence.EntityManager;
import javax.persistence.Query;
public class UserRepositoryImpl {
@Autowired
private EntityManager entityManager;
public Role findByRoleName2(String roleName) {
String jpql = "select r from Role r where r.roleName=:roleName";
Query query = entityManager.createQuery(jpql);
query.setParameter("roleName", roleName);
return (Role) query.getSingleResult();
}
}
注意实现类的写法,我们没有继承任何接口,只是以接口名Impl
命令了我们的实现类,并且实现类和接口在同一个包中,然后定义了一个相同名字的Java方法的实现,如果我们使用implements
实现RoleRepository
,我们需要同时实现大量其他方法(RoleRepository以及其父接口中定义的所有方法)才能编译通过,因此SpringDataJPA内部进行了一些处理,允许我们不使用implements
指定实现的接口,而保持接口和实现类按照约定命名即可。
这种写法有些脱离Java语法的一般常识了,个人认为是一个糟糕的设计,除非必要否则强烈不建议用。
插入使用save()
方法,和Hibernate(JPA)一样,例子代码如下:
Role role = new Role("student");
roleRepository.save(role);
我们执行删除操作时,通常也是要附加一些查询条件的,删除的写法和查询差不多:
/**
* 根据角色名删除角色
* @param roleName 角色名
* @return 受影响行数
*/
public int deleteByRoleName(String roleName);
修改的用法也和Hibernate(JPA)一样,临时态对象修改后使用save()
保存为持久态,Java对象的修改就会持久化到数据库中了。
QBE(Query By Example)是一种查询方式,能够直接通过模板对象,从数据库中匹配符合条件的记录,形成结果集。
下面是一段从项目中摘取的一段QBE查询的例子代码,主要实现了分页和条件查询:
@Override
public Map<String, Object> getFileRecordList(Integer pageSize, Integer currentPage, String namePattern, Integer isExist) {
// 排序分页
Sort sort = new Sort(Sort.Direction.ASC, "fileId");
PageRequest pageRequest = PageRequest.of(currentPage - 1, pageSize, sort);
// QBE查询
FileRecord fileRecord = new FileRecord();
fileRecord.setName(namePattern);
fileRecord.setIsExist(isExist);
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("name", ExampleMatcher.GenericPropertyMatchers.contains())
.withMatcher("isExist", ExampleMatcher.GenericPropertyMatchers.exact());
Example<FileRecord> example = Example.of(fileRecord, matcher);
Page<FileRecord> resultPage = fileRecordRepository.findAll(example, pageRequest);
Map<String, Object> resultMap = new HashMap<>();
// 所有记录
resultMap.put("records", resultPage.getContent());
// 当前页码
resultMap.put("currentPage", currentPage);
// 总记录数
resultMap.put("totalElements", resultPage.getTotalElements());
return resultMap;
}
有些很复杂的动态查询,JpaRepository和QBE都不能很好的满足我们的需求,而使用拼接JPQL或SQL也是相当麻烦而且不可取的,这时我们可以使用Criteria查询。在Spring Data JPA中,对Criteria操作进行了封装,使用起来还算简单。
想要使用Criteria查询的Repository接口,需要继承JpaSpecificationExecutor<>
接口,在repository实例上调用findAll()
方法时,传入一个Specification<>
对象。具体我们看一个例子,下面例子是来自实际项目中的一段代码,实现了共5个条件的动态查询,查询参数类型包括字符串模糊查询、字符串精确查询、日期比较查询、整数查询:
查询参数封装类型:QueryParam.java
public class QueryParam {
private Date startTime;
private Date endTime;
private String keyword;
private Integer publicStatus;
private Integer starStatus;
private Integer pageSize;
private Integer currentPage;
// ... 构造函数、GET/SET、toString等方法,为了节省篇幅此处省略
}
动态查询的写法:
@Override
public Map<String, Object> queryDiaryByPageAndParam(QueryParam queryParam) {
// 排序分页
Sort sort = new Sort(Sort.Direction.ASC, "diaryId");
PageRequest pageRequest = PageRequest.of(queryParam.getCurrentPage() - 1, queryParam.getPageSize(), sort);
// 动态查询
Page<Diary> queryResult = diaryRepository.findAll(new Specification<Diary>() {
@Override
public Predicate toPredicate(Root<Diary> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
List<Predicate> predicateList = new ArrayList<>();
// 时间字段大于起始时间
Predicate startTimePredicate = null;
if (queryParam.getStartTime() != null) {
startTimePredicate = criteriaBuilder.greaterThanOrEqualTo(root.<Date>get("createTime"), queryParam.getStartTime());
predicateList.add(startTimePredicate);
}
// 时间字段小于结束时间
Predicate endTimePredicate = null;
if (queryParam.getEndTime() != null) {
endTimePredicate = criteriaBuilder.lessThanOrEqualTo(root.<Date>get("createTime"), queryParam.getEndTime());
predicateList.add(endTimePredicate);
}
// 标题和内容中包含指定关键字
Predicate keywordPredicateInTitle = null;
Predicate keywordPredicateInContent = null;
if (queryParam.getKeyword() != null && !"".equals(queryParam.getKeyword())) {
keywordPredicateInTitle = criteriaBuilder.like(root.<String>get("title"), "%" + queryParam.getKeyword() + "%");
keywordPredicateInContent = criteriaBuilder.like(root.<String>get("content"), "%" + queryParam.getKeyword() + "%");
Predicate keywordPredicate = criteriaBuilder.or(keywordPredicateInTitle, keywordPredicateInContent);
predicateList.add(keywordPredicate);
}
// 具有指定的公开状态
Predicate publicStatusPredicate = null;
if (queryParam.getPublicStatus() != null) {
publicStatusPredicate = criteriaBuilder.equal(root.<Integer>get("isPublic"), queryParam.getPublicStatus());
predicateList.add(publicStatusPredicate);
}
// 具有指定的加星状态
Predicate starStatusPredicate = null;
if (queryParam.getStarStatus() != null) {
starStatusPredicate = criteriaBuilder.equal(root.<Integer>get("isStar"), queryParam.getStarStatus());
predicateList.add(starStatusPredicate);
}
// 动态查询条件合并
int predicateSize = predicateList.size();
if (predicateSize > 0) {
Predicate predicate = criteriaBuilder.and((Predicate[]) predicateList.toArray(new Predicate[predicateSize]));
// 将查询条件提交给查询计划
query.where(predicate);
}
return null;
}
}, pageRequest);
Map<String, Object> resultMap = new HashMap<>(64);
// 所有记录
resultMap.put("records", queryResult.getContent());
// 当前页码
resultMap.put("currentPage", queryParam.getCurrentPage());
// 总记录数
resultMap.put("totalElements", queryResult.getTotalElements());
return resultMap;
}
Spring Data JPA非常适合配合SpringBoot快速实现简单、不追求性能的需求;相反,在SQL查询十分复杂、性能要求高的场景,Spring Data JPA就不适用了,因为Spring Data JPA基于方法名自动实现JPQL查询难以表达查询参数非常多、非常复杂的情况,而编写实现类就和直接使用Hibernate(JPA)差不多麻烦了;除此之外,毕竟是基于Hibernate(JPA),性能优化也是个麻烦的问题,需要性能的情况推荐使用JDBC、MyBatis的方式。