一. 概述

Spring Data JPA 是 Java Persistence API (JPA) 规范的实现,底层是对Hibernate 5.x 操作数据库的封装,它简化了在java开发中使用 JPA 访问数据库的操作。

二. 使用 Spring Data Repositories

Spring Data repository 抽象的目的就是显著减少各种数据访问层实现技术的样板代码:

// Spring Data repository 是最基础的接口,用于获取repository管理的domain对象的类型以及id的类型
// 注意泛型的使用
public interface Repository<T, ID extends Serializable> {

}

CrudRepository接口继承了Repository接口,并提供了它所管理的domain对象的基本的 CRUD 方法,新版本中还添加了很多新的api,具体信息可以查询api:

// domain对象CRUD操作的泛型接口
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
    // 保存一个实体对象 insert into...
    <S extends T> S save(S entity);
    // 根据id获取domain对象,select * from xxx where id = ?
    Optional<T> findById(ID primaryKey);
    // 查询表中所有记录,将返回的结果集封装到一个集合对象中(List),select * from xxx
    Iterable<T> findAll();
    // 统计表中记录数
    long count();
    // 删除记录
    void delete(T entity);
    
    // ... lookup api for more details
}

PagingAndSortingRepository接口继承了CrudRepository接口并添加了分页查询的方法:

public interface PagingAndSortingRepository<T, ID extends Serializable> extends CrudRepository<T, ID> {
    // Pageable对象封装了分页查询的参数,用于SQL中limit子句的查询参数 page & size
    Page<T> findAll(Pageable pageable);
}

JpaRepository接口又继承了PagingAndSortingRepository,开发中用户自定义repository可以直接继承该接口,同时JpaRepository还继承了一个QueryByExampleExecutor接口,该接口提供对QBE查询方法的支持,比较非主流。

根据 JPA 规范,项目中的领域对象要添加注解,用于建立domain和数据库表之间的静态映射关系:

@Entity
@Table(name = "tb_user")
public class User implements Serializable {
	// id可以设置生成策略,这里选择通过id生成器手动设置
	@Id
	private String id;
	@Column	// 属性名称如果和表中字段名称一致可以不加注解
	private String username;
	@Column
	private String password;
	@Column
	private String email
	@Column
	private Date birthday;

	// ... constructor & methods
}

三. JPA Repositories

在使用 JPA 时Query对象的获取有两种方法,一种是手写SQL,另一种是从方法名称中派生(神奇)。从方法名称中派生的方式适用于查询条件不太复杂的情况,否者方法名称会变得很长很难看,还有在涉及关联查询的时候手写SQL更方便。

从查询方法名称中派生Query对象

public interface UserRepository extends JpaRepository<User, Long> {
    // 创建query对象操作JPA criteria api,该对象对应如下的JPQL查询语句
    // select from User u where u.emailAddress = ?1 and u.lastname = ?2
    List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
}

JPA使用“关键字”机制分割方法名称,建立多条件查询对象Query,该对象对应一条JQPL查询语句片段,关键字用于组合各种查询条件,JPA中关键字一共有二十多个,以下为常用的:

Keyword

Sample

JPQL snippet

And

findByLastnameAndFirstname

… where x.lastname = ?1 and x.firstname = ?2

Or

findByLastnameOrFirstname

… where x.lastname = ?1 or x.firstname = ?2

Is , Equals

findByFirstname , findByFirstnameEquals

… where x.firstname = ?1

LessThan

findByAgeLessThan

… where x.age < ?1

GrateThanEqual

findByAgeGreaterThanEqual

… where x.age >= ?1

Like

findByFirstnameLike

… where x.firstname like ?1

方法名称中可以使用关键字top或者first来过滤出查询结果的前n条记录:

List<User> findFirst10ByLastname(String lastname);

@Query 的使用方法

Spring Data JPA 中可以在查询方法上添加@Query注解来定义一条 JPQL 查询语句:

public interface UserRepository extends JpaRepository<User, Long> {
    // 注意模糊查询的使用
    @Query("select from User u where u.firstname like %?1")
    List<User> findByFirstnameEndsWith(String firstname);
}

@Query 注解允许创建原生的SQL查询语句而不是JPQL,使用原生的SQL查询语句的好处是可以在SQL图形化界面中调试好了之后再copy到代码中,防止出错。返回的结果集被自动映射封装为方法返回值声明类型的对象中。

public interface UserRepository extends JpaRepository<User, Long> {
    // 使用 nativeQuery = true 开启原生SQL语句查询
    @Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true)
    User findByEmailAddress(String emailAddress);
    
    @Modifying
    @Query("update User u set u.firstname = ?1 where u.lastname = ?2")
    int setFixedFirstnameFor(String firstname, String lastname);
}

@Modifying 注解

在@Query注解中编写 JPQL 实现 UPDATE 和 DELETE 的时候必须在方法上加上@Modifying 注解,通知 Spring Data 这是删除或者更新操作,同时在Service层需要添加事务的支持@Transactional,注意JPQL是不支持INSERT操作的,代码如上。

四. Specifications

JPA 2 规范中引入了一个条件查询的api,这个 criteria api 可以使用编程的方式手动设置查询的条件(where子句)。在 Spring Data JPA 中使用 criteria api 只需要继承接口JpaSpecificationExecutor,该接口中的查询方法接收一个Specification 类型的对象,这个对象中封装了where子句中的查询条件:

public interface CustomerRepository extends JpaRepository<Customer, Long>, JpaSpecificationExecutor {
    // 继承JpaSpecificationExecutor接口,实现条件查询,这个方法特别使用于页面多参数的
    // 条件查询,需要在运行中动态构建SQL语句,类似于mybatis中动态SQL语句
}

构建动态查询条件对象:

/**
 * 动态条件构建
 * @param searchMap
 * @return
 */
private Specification<Problem> createSpecification(Map searchMap) {

    return new Specification<Problem>() {

        @Nullable
        @Override
        public Predicate toPredicate(Root<Problem> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
            List<Predicate> predicateList = new ArrayList<Predicate>();
            // ID
            if (searchMap.get("id")!=null && !"".equals(searchMap.get("id"))) {
                predicateList.add(cb.like(root.get("id").as(String.class), "%"+(String)searchMap.get("id")+"%"));
            }
            // 标题
            if (searchMap.get("title")!=null && !"".equals(searchMap.get("title"))) {
                predicateList.add(cb.like(root.get("title").as(String.class), "%"+(String)searchMap.get("title")+"%"));
            }
            // 内容
            if (searchMap.get("content")!=null && !"".equals(searchMap.get("content"))) {
                predicateList.add(cb.like(root.get("content").as(String.class), "%"+(String)searchMap.get("content")+"%"));
            }
            // 用户ID
            if (searchMap.get("userid")!=null && !"".equals(searchMap.get("userid"))) {
                predicateList.add(cb.like(root.get("userid").as(String.class), "%"+(String)searchMap.get("userid")+"%"));
            }
            // 昵称
            if (searchMap.get("nickname")!=null && !"".equals(searchMap.get("nickname"))) {
                predicateList.add(cb.like(root.get("nickname").as(String.class), "%"+(String)searchMap.get("nickname")+"%"));
            }
            // 是否解决
            if (searchMap.get("solve")!=null && !"".equals(searchMap.get("solve"))) {
                predicateList.add(cb.like(root.get("solve").as(String.class), "%"+(String)searchMap.get("solve")+"%"));
            }
            // 回复人昵称
            if (searchMap.get("replyname")!=null && !"".equals(searchMap.get("replyname"))) {
                predicateList.add(cb.like(root.get("replyname").as(String.class), "%"+(String)searchMap.get("replyname")+"%"));
            }

            return cb.and( predicateList.toArray(new Predicate[predicateList.size()]));
        }
    };
}

然后调用条件查询:

/**
 * 条件查询+分页
 *
 * */
public Page<Problem> pageQuery(Map searchMap, int page, int size){
    Specification<Problem> specification = createSpecification(searchMap);
    Pageable pageable = PageRequest.of(page - 1, size);

    return problemDao.findAll(specification, pageable);
}