首页 > 编程笔记

Spring Data JPA简介与使用

Spring Data JPA 是 Spring Data 大家族中的一员,可以轻松实现基于 JPA 的存储库。Spring Data JPA 主要基于 JPA 提供对数据访问层的增强支持。借助它可以使构建设计数据访问技术的Spring应用程序变得更加容易。

在相当长的一段时间内,实现应用程序的数据访问层一直很麻烦。必须编写大量的样板代码来执行简单查询以及执行分页和审计。

Spring Data JPA 旨在通过减少实际需要的工作量来显著改善数据访问层的实现。作为开发人员可以只编写 Repository 接口,包括自定义查找器方法,Spring 将自动提供对应的实现。Spring Data 生态如图 1 所示。
图 1 Spring Data生态示意图
图 1 Spring Data生态示意图

Spring Data JPA 是 Spring Data 对 JPA 规范的封装,在其规范下提供 Repository 层的实现,并提供配置项用以切换具体实现规范的 ORM 框架。Spring Data JPA、JPA 以及基于 JPA 规范的 ORM 框架,如图 2 所示。

图2 Spring Data JPA、JPA与各ORM框架
图2 Spring Data JPA、JPA与各ORM框架

基于 JpaRepository 接口查询

Spring Data JPA 框架的目标之一就在于简化数据访问层的开发过程,消除项目中的样板代码。基于接口的查询方式是实现代码简化的有效方法。通过基于接口的查询方式进行开发,框架将在应用运行时,根据接口名的定义生成包含对应 SQL 语句的代理实例。这免去了手写 SQL 的环节,进而实现了简化。

基于接口查询,首先要关注的接口为“Repository”。Repository 是 Spring Data JPA 的核心接口。它需要领域实体类以及实体类的 ID 类型作为类型参数进行管理。该类主要作为标记接口,用以捕获要使用的类型并帮助发现扩展该接口的子接口。

另外还有更为具体的 CrudRepository 以及 JpaRepository,这两个类包含具体的基础 CURD 方法。

Repository.java:
@Indexed
public interface Repository<T, ID> {
}
JpaRepository.java:
@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {

    //查询所有数据
    @Override
    List<T> findAll();

    //查询所有数据,并以排序选项进行排序后返回
    @Override
    List<T> findAll(Sort sort);

    //根据id查询集合
    @Override
    List<T> findAllById(Iterable<ID> ids);

    //保存所有数据
    @Override
    <S extends T> List<S> saveAll(Iterable<S> entities);

    //将之前的改动刷写进数据库
    void flush();

    //保存并立刻刷写当前实体
    <S extends T> S saveAndFlush(S entity);

    //删除给出的集合
    void deleteInBatch(Iterable<T> entities);

    //批量删除
    void deleteAllInBatch();

    //根据id查询目标实体
    T getOne(ID id);

    //根据实例查询
    @Override
    <S extends T> List<S> findAll(Example<S> example);

    //根据实例查询并排序
    @Override
    <S extends T> List<S> findAll(Example<S> example, Sort sort);
}
使用基于接口的查询方式,首先需要定义查询表对应的实体。实体示例 Patient.java 如下:
@Entity
@Table(name = "patient")
@Data
@Accessors(chain = true)
public class Patient implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @Id
    @Column(name = "id", nullable = false)
    private Integer id;

    /**
     * 名
     */
    @Column(name = "first_name")
    private String firstName;

    /**
     * 姓
     */
    @Column(name = "last_name")
    private String lastName;

    /**
     * 身高
     */
    @Column(name = "height")
    private BigDecimal height;

    /**
     * 体重
     */
    @Column(name = "body_weight")
    private BigDecimal bodyWeight;

    /**
     * BMI指数
     */
    @Column(name = "BMI")
    private BigDecimal BMI;
}
通过注解 @Entity 标注该类为实体类,通过 @Table(name = "patient") 标注该类的表明为 patient。类的主键需要使用 @Id 注解进行标注,另外需要@Column 注解标注对应的字段名。实体查询类 PatientRepository.java 如下:
public interface PatientRepository extends JpaRepository<Patient, Integer> {

    List<Patient> findByFirstName(String firstName);

    Patient findByFirstNameAndLastName(String firstName, String lastName);

    List<Patient> findByHeightGreaterThan(BigDecimal height);
}
PatientRepository.java 对应测试代码如下:
@Test
public void testJpaRepository(){
        List<patient> san=patientRepository.findByFirstName("san");
        assert san!=null;
        assert san.size()>0;
        Patient lisi=patientRepository.findByFirstNameAndLastName("si","li");
        assert lisi!=null;
        List<Patient> tallPatients=patientRepository.findByHeightGreaterThan(new BigDecimal(190));
        assert tallPatients!=null;
        assert tallPatients.size()==0;
}
从以上示例可以观察到,完成一个查询仅需要定义一个 findBy{:column} 格式的方法名。事实上,findBy 可以替换为 getBy、readBy 或者直接去掉。

Spring Data JPA 将在应用运行时对方法名进行解析,解析的过程为:去掉 findBy 等前缀,再根据剩下的字段名与关键字,生成对应查询的代码实现。关键字及示例参考表 1 所示。
表1 关键字及示例参考
关键字 示例 JPQL语句片段
And findByLastnameAndFirstname ... where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname ... where x.lastname = ?1 or x.firstname = ?2
Is,Equals findByFirstname,findByFirstnameIs,findByFirstname
Equals
... where x.firstname = ?1
Between findByStartDateBetween ... where x.startDate between?1 and?2
LessThan findByAgeLessThan ... where x.age<?1
LessThanEqual findByAgeLessThanEqual ... where x.age<=?1
GreaterThan findByAgeGreaterThan ... where x.age>?1
GreaterThanEqual findByAgeGreaterThanEqual ... where x.age>= ?1
After findByStartDateAfter ... where x.startDate>?1
Before findByStartDateBefore ... where x.startDate<? 1
IsNull findByAgelsNull ... where x.age is null
IsNotNull,NotNull findByAge(Is)NotNull ... where x.age not null
Like findByFirstnameLike ... where x.firstname like?1
NotLike findByFirstnameNotLike ... where x.firstname not like ? 1
StartingWith findByFirstnameStartingWith ... where x.firstname like ?1 (parameter bound with appended %)
EndingWith findByFirstnameEndingWith ... where x.firstname like ?1 (parameter bound with prepended %)
Containing findByFirstnameContaining ... where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc ... where x.age = ?1 order by x.lastname desc
Not findByLastnameNot ... where x.lastname <> ? 1
In findByAgeIn(Collection<Age> ages) ... where x.age in?1
NotIn findByAgeNotIn(Collection<Age> age) ... where x.agenot in? 1
TRUE findByActiveTrue() ... where x.active = true
FALSE findByActiveFalse() ... where x.active = false
IgnoreCase findByFirstnamelgnoreCase ... where UPPER(x.firstame) = UPPER(?1)

基于JpaSpecificationExecutor接口查询

上面我们介绍的 JpaRepository 接口固然十分方便,但用于实现逻辑更为复杂的需求,便显得捉襟见肘了。使用 JpaRepository 接口更适用于参数不多、逻辑简单的查询场景。为了补足 JpaRepository 难以实现的部分,Spring Data JPA 另外提供了 JpaSpecificationExecutor 这一接口供复杂查询的场景使用。

JpaSpecificationExecutor.java:
public interface JpaSpecificationExecutor<T> {
    //根据spec查询出一个Optional的实体类
    Optional<T> findOne(@Nullable Specification<T> spec);

    //根据spec查询出对应实体列表
    List<T> findAll(@Nullable Specification<T> spec);

    //根据spec查询出实体分页
    Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);

    //根据spec查询出对应实体列表,并根据给出的排序条件进行排序
    List<T> findAll(@Nullable Specification<T> spec, Sort sort);

    //查询满足spec条件的实体列表长度
    long count(@Nullable Specification<T> spec);
}
其中 Specification 接口提供的 toPredicate() 方法,供开发人员灵活构造复杂的查询条件。

Specification.java:
public interface Specification<T> extends Serializable {

    @Nullable
    Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
    //省略若干方法
}
在基于接口查询的开发过程中,往往会在实体的 Repository 接口类同时继承 JpaRepository 与 JpaSpecificationExecutor,以赋予该 Repository 接口能同时完成简单查询与复杂查询的能力。

UserRepository.java:
 public interface UserRepository extends JpaRepository<User, Integer>,JpaSpecificationExecutor<User> {
 }
实体类User.java:
@Entity
@Table(name = "user")
@Data
@Accessors(chain = true)
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @Id
    @Column(name = "id", nullable = false)
    private Integer id;

    /**
     * 用户名
     */
    @Column(name = "name")
    private String name;

    /**
     * 账户名
     */
    @Column(name = "account")
    private String account;

    /**
     * 密码
     */
    @Column(name = "password")
    private String password;

    /**
     * 创建时间
     */
    @Column(name = "create time")
    private LocalDateTime createTime;

    @OneToOne
    private Vehicle vehicle;
}
设想一个场景,需要创建一个查询方法用于查询符合条件的用户。例如,给出一个时间区间与一个关键字,查询出创建时间在该区间内的所有用户,并且用户名包含关键字。如果使用 JpaRepository 实现,则示例代码如下:
@Test
public void testJpaRepositoryComplicated(){
    //根据时间与关键字查询
    List<User> queryWithTimeAndKeyWord=getUser(LocalDateTime.of(2019,1,1,0,0,0),LocalDateTime.of(2020,1,1,0,0,0),"陈");
    assert queryWithTimeAndKeyWord !=null;
    //根据时间查询
    List<User> queryWithTime=getUser(LocalDateTime.of(2019,1,1,0,0,0),LocalDateTime.of(2020,1,1,0,0,0),null);
    assert queryWithTime!=null;
    //普通查询
    List<User> query=getUser(null,null,"张");
    assert query!=null;
}

//以下示例为不推荐的查询实现方式,属于错误示范
private List<User> getUser(@Nullable LocalDateTime start,@Nullable LocalDateTime end,@Nullable String keyword){
    //String.format中%为特殊字符需要再加一个%进行转义,以下格式化的结果为% {keyword} %
    String nameLike = keyword == null ? null : String. format ("%%%s%%", keyword);
    if (start != null && end != null && !StringUtils.isEmpty(nameLike)) {
        //查询条件同时包含时间与关键字
        return userRepository.findByCreateTimeBetweenAndNameLike(start,end,nameLike);
    }else if(start! =null&&end! =null&&StringUtils.isEmpty(nameLike)){
        //查询条件仅包含时间
        return userRepository.findByCreateTimeBetween(start,end);
    }else if((start==null||end==null)&&!StringUtils.isEmpty(nameLike)){
        //查询条件仅包含关键字
        return userRepository.findByNameLike(keyword);
    }else{
        return userRepository.findAll();
    }
}
可以看到,这一段代码的实现并不优雅,需要针对不同情况定义不同的 Repository 接口。如果参数进一步增加,对应 Repository 接口内的方法数量将膨胀到难以维护的程度。使用 JpaSpecificationExecutor 实现同样的功能,示例代码如下:
@Test
public void testJpaSpecificmtionExecutor(){
    //根据时间与关键字查询
    List<User> queryWithTimeAndKeyWord = getUserWithJpaSpecificationExecutor(LocalDateTime.of(2019,1,1,0,0,0),LocalDateTime.of(2020,1,1,0,0,0),"陈");
    assert queryWithTimeAndKeyWord!=null;
    //根据时间查询
    List<Use:r>queryWithTime=getUserWithJpaSpecificationExecutor(LocalDateTime.of(2019,1,1,0,0,0),LocalDateTime.of(2020,1,1,0,0,0),null);
    assert queryWithTime!=null;
    //普通查询
    List<User> query=getUserWithJpaSpecificationExecutor(null,null,"张"); assert query != null;
}

private List<User> getUserWithJpaSpecificationExecutor(@Nullable LocalDateTime start,@Nullable LocalDateTime end,@Nullable String keyword){
    //String, format中%为特殊字符需要再加一个%进行转义,以下格式化的结果为% {keyword} %
    String nameLike=keyword==null?null:String.format("%%%s%%",keyword);
    return userRepository.findAll(((root,query,criteriaBuilder) ->{
        //根据传入参数的不同构造谓词列表
        List<Predicate> predicates=new ArrayList<>();
        if(start!=null&&end!=null){
            predicates.add(criteriaBuilder.between(root.get("createTime"),start,end));
        }
        if(!StringUtils.isEmpty(nameLike)){
            predicates.add(criteriaBuilder.like(root.get("nmine"),nameLike));
        }
        query.where(predicates.toArray(new Predicate[0]));
        return query.getRestriction();
    }));
}
较之于 JpaRepository 的查询方式,JpaSpecificationExecutor 并不需要另外定义接口,通过组合各种谓词(Predicate)构造最终的查询条件。

优秀文章