Fellow Travellers

JPA的简单介绍

殷萧鹏
字数统计: 4k阅读时长: 17 min
2018/11/01 Share

JPA的简单介绍

简介

jpa定义

JPA(Java Persistence API)是Sun官方提出的Java持久化规范。它为Java开发人员制订了一些规范,提供了一些编程的API接口来管理Java应用中的关系数据。

jpa/orm二者间的关系

JPA不是一种新的ORM框架,它的出现只是用于规范现有的ORM技术,它不能取代现有的Hibernate等ORM框架,相反,采用JPA开发时,我们仍将使用这些ORM框架,只是此时开发出来的应用不在依赖于某个持久化提供商。应用可以在不修改代码的情况下在任何JPA环境下运行,真正做到低耦合,可扩展的程序设计。类似于JDBC,在JDBC出现以前,我们的程序针对特性的数据库API进行编程,但是现在我们只需要针对JDBC API编程,这样能够在不改变代码的情况下就能换成其他的数据库。

jpa的优势

jpa的出现主要是为了简化现有的持久化开发工作和整合ORM技术,结束现在Hibernate、TopLink等ORM框架各自为营的局面。JPA是在充分吸收了现有Hibernate、TopLink等ORM框架的基础上发展起来的,具有易于使用,伸缩性强等优点。

标准化
JPA 是 JCP 组织发布的 Java EE 标准之一,因此任何声称符合 JPA 标准的框架都遵循同样的架构,提供相同的访问API,这保证了基于JPA开发的企业应用能够经过少量的修改就能够在不同的JPA框架下运行。
简单方便
JPA的主要目标之一就是提供更加简单的编程模型:在JPA框架下创建实体和创建Java 类一样简单,没有任何的约束和限制,只需要使用 javax.persistence.Entity进行注释,JPA的框架和接口也都非常简单,没有太多特别的规则和设计模式的要求,开发者可以很容易地掌握。JPA基于非侵入式原则设计,因此可以很容易地和其它框架或者容器集成。
查询能力
JPA的查询语言是面向对象而非面向数据库的,它以面向对象的自然语法构造查询语句,可以看成是Hibernate HQL的等价物。JPA定义了独特的JPQL(Java Persistence Query Language),JPQL是EJB QL的一种扩展,它是针对实体的一种查询语言,操作对象是实体,而不是关系数据库的表,而且能够支持批量更新和修改、JOIN、GROUP BY、HAVING 等通常只有 SQL 才能够提供的高级查询特性,甚至还能够支持子查询。
高级特性
JPA 中能够支持面向对象的高级特性,如类之间的继承、多态和类之间的复杂关系,这样的支持能够让开发者最大限度的使用面向对象的模型设计企业应用,而不需要自行处理这些特性在关系数据库的持久化。

Spring Data JPA实现

Repository是jpa中定义用来访问领域对象的一个类似集合的接口,这个叫法就类似于我们通常所说的DAO。spring Data 基于 ORM 框架、JPA 规范的基础上封装的一套JPA应用框架,给我们提供几个Repository,基础的Repository提供了最基本的数据访问功能,其几个子接口则扩展了一些功能。它们的关系如下:

Repository: 仅仅是一个标识,表明任何继承它的均为仓库接口类,方便Spring自动扫描识别 ;CrudRepository: 继承Repository,实现了一组CRUD相关的方法
PagingAndSortingRepository: 继承CrudRepository,实现了一组分页排序相关的方法 ;
JpaRepository: 继承PagingAndSortingRepository,实现一组JPA规范相关的方法;

虽然ORM框架实现了JPA规范,但是在不同ORM框架之间切换需要编写的代码有一些差异,而通过使用Spring Data JPA能够方便大家在不同的ORM框架中间进行切换而不要更改代码。可使开发者用极简的代码即可实现对数据的访问和操作。

JpaRepository接口规范

JpaRepository作为Spring Data JPA提供的对Repository的实现之一,是我们主要的操作对象。在代码中dao层继承JpaRepository,利用JpaRepository支持接口规范方法名查询特性,可以做到只在接口中定义的查询方法符合它的命名规则,就可以不用写实现。比如,当你看到 UserDao.findUserById() 这样一个方法声明,大致应该能判断出这是根据给定条件的 ID 查询出满足条件的 User 对象。Spring Data JPA 做的便是规范方法的名字,根据符合规范的名字来确定方法需要实现什么样的逻辑。 大大简化了开发过程。类似这种UserDao.findUserById()规范方法名,Spring Data JPA还提供了另外两种规范,分别是命名查询规范(@NamedQuery)和@Query 注解 方式。下面我们分别讲述三种创建查询方式的解析流程。

方法名解析

框架在进行方法名解析时,会先把方法名多余的前缀截取掉,比如 find、findBy、read、readBy、get、getBy,然后对剩下部分进行解析。并且如果方法的最后一个参数是 Sort 或者 Pageable 类型,也会提取相关的信息,以便按规则进行排序或者分页查询。

在创建查询时,我们通过在方法名中使用属性名称来表达,比如 findByUserAddressZip ()。框架在解析该方法时,首先剔除 findBy,然后对剩下的属性进行解析,详细规则如下(此处假设该方法针对的域对象为 AccountInfo 类型):

  • 先判断 userAddressZip (根据 POJO 规范,首字母变为小写,下同)是否为 AccountInfo 的一个属性,如果是,则表示根据该属性进行查询;如果没有该属性,继续第二步;
  • 从右往左截取第一个大写字母开头的字符串(此处为 Zip),然后检查剩下的字符串是否为 AccountInfo 的一个属性,如果是,则表示根据该属性进行查询;如果没有该属性,则重复第二步,继续从右往左截取;最后假设 user 为 AccountInfo 的一个属性;
  • 接着处理剩下部分( AddressZip ),先判断 user 所对应的类型是否有 addressZip 属性,如果有,则表示该方法最终是根据 “AccountInfo.user.addressZip” 的取值进行查询;否则继续按照步骤 2 的规则从右往左截取,最终表示根据 “AccountInfo.user.address.zip” 的值进行查询。

可能会存在一种特殊情况,比如 AccountInfo 包含一个 user 的属性,也有一个 userAddress 属性,此时会存在混淆。读者可以明确在属性之间加上 “_” 以显式表达意图,比如 “findByUser_AddressZip()” 或者 “findByUserAddress_Zip()”。

在查询时,通常需要同时根据多个属性进行查询,且查询的条件也格式各样(大于某个值、在某个范围等等),Spring Data JPA 为此提供了一些表达条件查询的关键字,大致如下:

  • And — 等价于 SQL 中的 and 关键字,比如 findByUsernameAndPassword(String user, Striang pwd);
  • Or — 等价于 SQL 中的 or 关键字,比如 findByUsernameOrAddress(String user, String addr);
  • Between — 等价于 SQL 中的 between 关键字,比如 findBySalaryBetween(int max, int min);
  • LessThan — 等价于 SQL 中的 “<”,比如 findBySalaryLessThan(int max);
  • GreaterThan — 等价于 SQL 中的”>”,比如 findBySalaryGreaterThan(int min);
  • IsNull — 等价于 SQL 中的 “is null”,比如 findByUsernameIsNull();
  • IsNotNull — 等价于 SQL 中的 “is not null”,比如 findByUsernameIsNotNull();
  • NotNull — 与 IsNotNull 等价;
  • Like — 等价于 SQL 中的 “like”,比如 findByUsernameLike(String user);
  • NotLike — 等价于 SQL 中的 “not like”,比如 findByUsernameNotLike(String user);
  • OrderBy — 等价于 SQL 中的 “order by”,比如 findByUsernameOrderBySalaryAsc(String user);
  • Not — 等价于 SQL 中的 “! =”,比如 findByUsernameNot(String user);
  • In — 等价于 SQL 中的 “in”,比如 findByUsernameIn(Collection userList) ,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数;
  • NotIn — 等价于 SQL 中的 “not in”,比如 findByUsernameNotIn(Collection userList) ,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数;

@NamedQuery解析

命名查询是 JPA 提供的一种将查询语句从方法体中独立出来,以供多个方法共用的功能。Spring Data JPA 对命名查询也提供了很好的支持。用户只需要按照 JPA 规范在 orm.xml 文件或者在代码中使用 @NamedQuery(或 @NamedNativeQuery)定义好查询语句,唯一要做的就是为该语句命名时,需要满足”DomainClass.methodName()”的命名规则。假设定义了如下接口:

1
2
3
public interface UserDao extends JpaRepository<AccountInfo, Long> {
public List<AccountInfo> findTop5();
}

如果希望为 findTop5() 创建命名查询,并与之关联,我们只需要在适当的位置定义命名查询语句,并将其命名为 “AccountInfo.findTop5”,框架在创建代理类的过程中,解析到该方法时,优先查找名为 “AccountInfo.findTop5” 的命名查询定义,如果没有找到,则尝试解析方法名。可以在创建entity的时候使用@NamedQuery注解,如下所示:

1
2
3
4
5
@NamedQuery(name ="AccountInfo.findTop5",query = "select a from AccountInfo a where a.sort > 5")
public class AccountInfo{
private Integer accountId;
...
}

@Query解析

@Query 注解的使用非常简单,只需在声明的方法上面标注该注解,同时提供一个 JPQL 查询语句即可,如下所示:

1
2
3
4
5
6
7
public interface UserDao extends JpaRepository<AccountInfo, Long> {
@Query("select a from AccountInfo a where a.accountId = ?1")
public AccountInfo findByAccountId(Long accountId);

@Query("select a from AccountInfo a where a.balance > ?1")
public Page<AccountInfo> findByBalanceGreaterThan(Integer balance,Pageable pageable);
}

很多开发者在创建 JPQL 时喜欢使用命名参数来代替位置编号,@Query 也对此提供了支持。JPQL 语句中通过”: 变量”的格式来指定参数,同时在方法的参数前面使用 @Param 将方法参数与 JPQL 中的命名参数对应,示例如下:

1
2
3
4
5
6
7
public interface UserDao extends JpaRepository<AccountInfo, Long> {
@Query("from AccountInfo a where a.accountId = :id")
public AccountInfo findByAccountId(@Param("id")Long accountId);

@Query("from AccountInfo a where a.balance > :balance")
public Page<AccountInfo> findByBalanceGreaterThan(@Param("balance")Integer balance,Pageable pageable);
}

此外,也可以通过使用 @Query 来执行一个更新操作,为此,我们需要在使用 @Query 的同时,用 @Modifying 来将该操作标识为修改查询,这样框架最终会生成一个更新的操作,而非查询。如下所示:

1
2
3
4
5
public interface UserDao extends JpaRepository<AccountInfo, Long> {
@Modifying(clearAutomatically = true)
@Query("update AccountInfo a set a.salary = ?1 where a.salary < ?2")
public int increaseSalary(int after, int before);
}

如果@Query注解加上nativeQuery=true 则查询语句使用原生sql,不加则使用JPQL

1
2
3
4
5
public interface UserDao extends JpaRepository<AccountInfo, Long> {
@Modifying(clearAutomatically = true)
@Query("update account_table a set a.salary = ?1 where a.salary < ?2",nativeQuery=true)
public int increaseSalary(int after, int before);
}

(注:@Modifying(clearAutomatically = true) 自动清除实体里保存的数据。)

JpaSpecificationExecutor接口规范

JpaSpecificationExecutor不属于Repository体系,它是Spring Data JPA为支持JPA2.0的Criteria查询,提供的接口。JpaSpecificationExecutor接口只定义了几个简单的方法,其核心都是围绕着Specification来实现。

1
2
3
4
5
6
7
8
9
Optional<T> findOne(@Nullable Specification<T> var1);

List<T> findAll(@Nullable Specification<T> var1);

Page<T> findAll(@Nullable Specification<T> var1, Pageable var2);

List<T> findAll(@Nullable Specification<T> var1, Sort var2);

long count(@Nullable Specification<T> var1);

Specification接口中定义了如下一个方法:

1
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);

要理解这个方法,以及正确的使用它,就需要对JPA2.0的Criteria查询有一个足够的熟悉和理解,因为这个方法的参数和返回值都是JPA标准里面定义的对象。

应用

简单查询

使用JpaRepository接口进行简单查询操作:

1
2
3
4
5
public class Banner implements Serializable{
private String id;
private Integer sort; // 排序
...
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface IBannerDao extends JpaRepository<Banner,String>,JpaSpecificationExecutor<Banner> {

/**
* 查询当前sort的最大值
* @return
*/
@Query(value = "SELECT MAX(SORT) FROM banner",nativeQuery = true)
Integer findMaxSort();

/**
* 根据sort获取banner
* @param sotr
* @return
*/
Banner findBannerBySort(Integer sotr);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class BannerService{
@Resource
private IBannerDao bannerDao;

/**
* 根据排序sort获取banner
* @param sort
* @return
*/
public Banner getBannerBySort(Integer sort) {
return bannerDao.findBannerBySort(sort);
}

/**
* 查询当前sort的最大值
* @param sort
* @return
*/
public Integer getMaxSort(Integer sort) {
return bannerDao.findMaxSort();
}

...
}

动态查询

使用JpaSpecificationExecutor接口进行动态查询操作:

1
2
3
4
5
6
public class User implements Serializable{
private String id;
private String userName;
private String passWord;
...
}
1
2
3
public interface IUserDao extends JpaRepository<User,String>,JpaSpecificationExecutor<User> {

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class UserService{
@Resource
private IUserDao UserDao;

/**
* 多条件查询用户
* @param user
* @return
*/
public User getUsersByMultiCondition(User user) {
Optional<User> optional = userDao.findOne(new Specification<User>() {
@Nullable
@Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
List<Predicate> predicateList = new ArrayList<>();
if (ObjectUtil.isNonNull(user.getId())) {
predicateList.add(criteriaBuilder.equal(root.get("id"), user.getId()));
}
if (ObjectUtil.isNonNull(user.getPassWord())) {
predicateList.add(criteriaBuilder.equal(root.get("passWord"), user.getPassWord()));
}
if (ObjectUtil.isNonNull(user.getUserName())) {
predicateList.add(criteriaBuilder.equal(root.get("userName"), user.getUserName()));
}
Predicate[] predicates = new Predicate[predicateList.size()];
return criteriaBuilder.and(predicateList.toArray(predicates));
}
});
return optional.get();
}
...

}

在构建动态查询条件时,主要是通过CriteriaBuilder生成条件对象Predicate。查看CriteriaBuilder源码可看到内部提供了生成不同Predicate对象的方法,通过CriteriaBuilder可实现构建灵活的动态查询语句。

1
2
3
4
5
6
7
8
9
10
11
12
Predicate and(Expression<Boolean> var1, Expression<Boolean> var2);
Predicate or(Expression<Boolean> var1, Expression<Boolean> var2); Predicate not(Expression<Boolean> var1);
Predicate isNull(Expression<?> var1);
Predicate isNotNull(Expression<?> var1);
Predicate equal(Expression<?> var1, Object var2);
Predicate notEqual(Expression<?> var1, Object var2);
<Y extends Comparable<? super Y>> Predicate greaterThan(Expression<? extends Y> var1, Y var2);
<Y extends Comparable<? super Y>> Predicate lessThan(Expression<? extends Y> var1, Y var2);
<Y extends Comparable<? super Y>> Predicate between(Expression<? extends Y> var1, Y var2, Y var3);
Predicate gt(Expression<? extends Number> var1, Number var2);
Predicate like(Expression<String> var1, String var2);
...

复杂查询

分页动态查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class UserService{
@Resource
private IUserDao UserDao;

/**
* 分页多条件查询用户
* @param user
* @return
*/
public List<User> getUsersByMultiCondition(User user) {
int pageSize = 10
int currentPage = 1
Pageable pageable = PageRequest.of(currentPage,pageSize);
Page<User> result = userDao.findOne(new Specification<User>() {
@Nullable
@Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
List<Predicate> predicateList = new ArrayList<>();
if (ObjectUtil.isNonNull(user.getId())) {
predicateList.add(criteriaBuilder.notEqual(root.get("id"), user.getId()));
}
Predicate[] predicates = new Predicate[predicateList.size()];
return criteriaBuilder.and(predicateList.toArray(predicates));
}
},pageable);
return result.getContent();
}
...

}

排序动态查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class UserService{
@Resource
private IUserDao UserDao;

/**
* 多条件查询用户并排序
* @param user
* @return
*/
public List<User> getUsersByMultiCondition(User user) {
Sort sort = Sort.by(Sort.Direction.DESC, "createTime");
List<User> result = userDao.findOne(new Specification<User>() {
@Nullable
@Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
List<Predicate> predicateList = new ArrayList<>();
if (ObjectUtil.isNonNull(user.getId())) {
predicateList.add(criteriaBuilder.notEqual(root.get("id"), user.getId()));
}
Predicate[] predicates = new Predicate[predicateList.size()];
return criteriaBuilder.and(predicateList.toArray(predicates));
}
},sort);
return result;
}
...

}

多表关联动态查询

1
2
3
4
5
6
public class Company implements Serializable {
private String id;
private String companyName;
private List<Employee> employees;

}

1
2
3
4
5
public class Employee implements Serializable {
private String id;
private String employeeName;
private Company company;
}
1
2
3
public interface ICompanyDao extends JpaRepository<Company,String>,JpaSpecificationExecutor<Company> {

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class CompanyService {

@Resource
private ICompanyDao companyDao;

public List<Company> getCompanyByMultiCondition(Map<String,String> params) {
List<Company> companyList = companyDao.findAll(new Specification<Company>() {
@Nullable
@Override
public Predicate toPredicate(Root<Company> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
Join<Company,Employee> companyEmployeeJoin = root.join(root.getModel().getList("employees", Employee.class),JoinType.LEFT);
List<Predicate> predicates = new ArrayList<>();
if (ObjectUtil.isNonNull(params.get("companyName"))) {
predicates.add(criteriaBuilder.like(root.get("companyName"),"%"+params.get("companyName")+"%" ));
}
if (ObjectUtil.isNonNull(params.get("employeeName"))) {
predicates.add(criteriaBuilder.like(companyEmployeeJoin.get("employeeName"),"%"+params.get("employeeName")+"%" ));
}
Predicate[] p = new Predicate[predicates.size()];
return criteriaBuilder.and(predicates.toArray(p));
}
});
return companyList;
}
}

在上面使用root.join实现多表之间的连接

1
Join<Company,Employee> companyEmployeeJoin = 					root.join(root.getModel().getList("employees", Employee.class),JoinType.LEFT);

root.getModel()指的是Company模型的元模型,等同于重新建立了一个类Company ;root.getModel().getList(“employees”, Employee.class)指的是将Company中的属性employees取出转换成Employee类,JoinType.LEFT指定了采用左连接方式,所以最终Company和 Employee建立了左关联联系,companyEmployeeJoin代表的就是 Employee类;上面定义后,等同于 sql语句:select c.* from t_company c left outer join t_employee e on c.id = e.foreign_id 。接着就是怎么带入动态参数即可。

CATALOG
  1. 1. JPA的简单介绍
    1. 1.1. 简介
      1. 1.1.1. jpa定义
      2. 1.1.2. jpa/orm二者间的关系
      3. 1.1.3. jpa的优势
      4. 1.1.4. Spring Data JPA实现
    2. 1.2. JpaRepository接口规范
      1. 1.2.1. 方法名解析
      2. 1.2.2. @NamedQuery解析
      3. 1.2.3. @Query解析
    3. 1.3. JpaSpecificationExecutor接口规范
    4. 1.4. 应用
      1. 1.4.1. 简单查询
      2. 1.4.2. 动态查询
      3. 1.4.3. 复杂查询