前面介绍了 Spring Data JPA 的使用方式和基本查询,常用的增、删、改、查需求 Spring Data JPA 已经实现了。但对于复杂的数据库场景,动态生成方法不能满足,对此 Spring Data JPA 提供了其他的解决方案,这就是这节课的主要内容。
使用 Spring Data 大部分的 SQL 都可以根据方法名定义的方式来实现,但是由于某些原因必须使用自定义的 SQL 来查询,Spring Data 也可以完美支持。
在 SQL 的查询方法上面使用 @Query 注解,在注解内写 Hql 来查询内容。
@Query("select u from User u")
Page findALL(Pageable pageable);
当然如果感觉使用原生 SQL 更习惯,它也是支持的,需要再添加一个参数 nativeQuery = true。
@Query("select * from user u where u.nick_name = ?1", nativeQuery = true)
Page findByNickName(String nickName, Pageable pageable);
@Query 上面的 1 代表的是方法参数里面的顺序,如果有多个参数也可以按照这个方式添加 1、2、3…。除了按照这种方式传参外,还可以使用 @Param 来支持。
@Query("select u from User u where u.nickName = :nickName")
Page findByNickName(@Param("nickName") String nickName, Pageable pageable);
如涉及到删除和修改需要加上 @Modifying,也可以根据需要添加 @Transactional 对事务的支持、操作超时设置等。
@Transactional(timeout = 10)
@Modifying
@Query("update User set userName = ?1 where id = ?2")
int modifyById(String userName, Long id);
@Transactional
@Modifying
@Query("delete from User where id = ?1")
void deleteById(Long id);
使用已命名的查询
除了使用 @Query 注解外,还可以预先定义好一些查询,并为其命名,然后再 Repository 中添加相同命名的方法。
定义命名的 Query:
@Entity
@NamedQueries({
@NamedQuery(name = "User.findByPassWord", query = "select u from User u where u.passWord = ?1"),
@NamedQuery(name = "User.findByNickName", query = "select u from User u where u.nickName = ?1"),
})
public class User {
……
}
通过 @NamedQueries 注解可以定义多个命名 Query,@NamedQuery 的 name 属性定义了 Query 的名称,注意加上 Entity 名称 . 作为前缀,query 属性定义查询语句。
定义对应的方法:
List findByPassWord(String passWord);
List findByNickName(String nickName);
Query 查找策略
到此,我们有了三种方法来定义 Query:
(1)通过方法名自动创建 Query,
(2)通过 @Query 注解实现自定义 Query,
(3)通过 @NamedQuery 注解来定义 Query。
那么,Spring Data JPA 如何来查找这些 Query 呢?
通过配置 @EnableJpaRepositories 的 queryLookupStrategy 属性来配置 Query 查找策略,有如下定义。
一般情况下使用默认配置即可,如果确定项目 Query 的具体定义方式,可以更改上述配置,例如,全部使用 @Query 来定义查询,又或者全部使用命名的查询。
Spring Data JPA 已经帮我们内置了分页功能,在查询的方法中,需要传入参数 Pageable,当查询中有多个参数的时候 Pageable 建议作为最后一个参数传入。
@Query("select u from User u")
Page findALL(Pageable pageable);
Page findByNickName(String nickName, Pageable pageable);
Pageable 是 Spring 封装的分页实现类,使用的时候需要传入页数、每页条数和排序规则,Page 是 Spring 封装的分页对象,封装了总页数、分页数据等。返回对象除使用 Page 外,还可以使用 Slice 作为返回值。
Slice findByNickNameAndEmail(String nickName, String email,Pageable pageable);
Page 和 Slice 的区别如下。
Page 接口继承自 Slice 接口,而 Slice 继承自 Iterable 接口。
Page 接口扩展了 Slice 接口,添加了获取总页数和元素总数量的方法,因此,返回 Page 接口时,必须执行两条 SQL,一条复杂查询分页数据,另一条负责统计数据数量。
返回 Slice 结果时,查询的 SQL 只会有查询分页数据这一条,不统计数据数量。
用途不一样:Slice 不需要知道总页数、总数据量,只需要知道是否有下一页、上一页,是否是首页、尾页等,比如前端滑动加载一页可用;而 Page 知道总页数、总数据量,可以用于展示具体的页数信息,比如后台分页查询。
@Test
public void testPageQuery() {
int page=1,size=2;
Sort sort = new Sort(Sort.Direction.DESC, “id”);
Pageable pageable = PageRequest.of(page, size, sort);
userRepository.findALL(pageable);
userRepository.findByNickName(“aa”, pageable);
}
Sort,控制分页数据的排序,可以选择升序和降序。
PageRequest,控制分页的辅助类,可以设置页码、每页的数据条数、排序等。
还有一些更简洁的方式来排序和分页查询,如下。
限制查询
有时候我们只需要查询前 N 个元素,或者只取前一个实体。
User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
Page queryFirst10ByLastname(String lastname, Pageable pageable);
List findFirst10ByLastname(String lastname, Sort sort);
List findTop10ByLastname(String lastname, Pageable pageable);
我们可以通过 AND 或者 OR 等连接词来不断拼接属性来构建多条件查询,但如果参数大于 6 个时,方法名就会变得非常的长,并且还不能解决动态多条件查询的场景。到这里就需要给大家介绍另外一个利器 JpaSpecificationExecutor 了。
JpaSpecificationExecutor 是 JPA 2.0 提供的 Criteria API 的使用封装,可以用于动态生成 Query 来满足我们业务中的各种复杂场景。Spring Data JPA 为我们提供了 JpaSpecificationExecutor 接口,只要简单实现 toPredicate 方法就可以实现复杂的查询。
我们来看一下 JpaSpecificationExecutor 的源码:
public interface JpaSpecificationExecutor {
//根据 Specification 条件查询单个对象,注意的是,如果条件能查出来多个会报错
T findOne(@Nullable Specification spec);
//根据 Specification 条件查询 List 结果
List findAll(@Nullable Specification spec);
//根据 Specification 条件,分页查询
Page findAll(@Nullable Specification spec, Pageable pageable);
//根据 Specification 条件,带排序的查询结果
List findAll(@Nullable Specification spec, Sort sort);
//根据 Specification 条件,查询数量
long count(@Nullable Specification spec);
}
JpaSpecificationExecutor 的源码很简单,根据 Specification 的查询条件返回 List、Page 或者 count 数据。在使用 JpaSpecificationExecutor 构建复杂查询场景之前,我们需要了解几个概念:
使用案例
下面的使用案例中会报错这几个对象的使用。
首先定义一个 UserDetail 对象,作为演示的数据模型。
@Entity
public class UserDetail {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false, unique = true)
private Long userId;
private Integer age;
private String realName;
private String status;
private String hobby;
private String introduction;
private String lastLoginIp;
}
创建 UserDetail 对应的 Repository:
public interface UserDetailRepository extends JpaSpecificationExecutor,JpaRepository {
}
定义一个查询 Page 的接口:
public interface UserDetailService {
public Page findByCondition(UserDetailParam detailParam, Pageable pageable);
}
在 UserDetailServiceImpl 中,我们来演示 JpaSpecificationExecutor 的具体使用。
@Service
public class UserDetailServiceImpl implements UserDetailService{
@Resource
private UserDetailRepository userDetailRepository;
@Override
public Page findByCondition(UserDetailParam detailParam, Pageable pageable){
return userDetailRepository.findAll((root, query, cb) -> {
List predicates = new ArrayList();
//equal 示例
if (!StringUtils.isNullOrEmpty(detailParam.getIntroduction())){
predicates.add(cb.equal(root.get("introduction"),detailParam.getIntroduction()));
}
//like 示例
if (!StringUtils.isNullOrEmpty(detailParam.getRealName())){
predicates.add(cb.like(root.get("realName"),"%"+detailParam.getRealName()+"%"));
}
//between 示例
if (detailParam.getMinAge()!=null && detailParam.getMaxAge()!=null) {
Predicate agePredicate = cb.between(root.get("age"), detailParam.getMinAge(), detailParam.getMaxAge());
predicates.add(agePredicate);
}
//greaterThan 大于等于示例
if (detailParam.getMinAge()!=null){
predicates.add(cb.greaterThan(root.get("age"),detailParam.getMinAge()));
}
return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
}, pageable);
}
}
上面的示例是根据不同条件来动态查询 UserDetail 分页数据,UserDetailParam 是参数的封装,示例中使用了常用的大于、like、等于等示例,根据这个思路我们可以不断扩展完成更复杂的动态 SQL 查询。
使用时只需要将 UserDetailService 注入调用相关方法即可:
@RunWith(SpringRunner.class)
@SpringBootTest
public class JpaSpecificationTests {
@Resource
private UserDetailService userDetailService;
@Test
public void testFindByCondition() {
int page=0,size=10;
Sort sort = new Sort(Sort.Direction.DESC, "id");
Pageable pageable = PageRequest.of(page, size, sort);
UserDetailParam param=new UserDetailParam();
param.setIntroduction("程序员");
param.setMinAge(10);
param.setMaxAge(30);
Page page1=userDetailService.findByCondition(param,pageable);
for (UserDetail userDetail:page1){
System.out.println("userDetail: "+userDetail.toString());
}
}
}
多表查询
多表查询在 Spring Data JPA 中有两种实现方式,第一种是利用 Hibernate 的级联查询来实现,第二种是创建一个结果集的接口来接收连表查询后的结果,这里主要介绍第二种方式。
我们还是使用上面的 UserDetail 作为数据模型来使用,定义一个结果集的接口类,接口类的内容来自于用户表和用户详情表。
public interface UserInfo {
String getUserName();
String getEmail();
String getAddress();
String getHobby();
}
在运行中 Spring 会给接口(UserInfo)自动生产一个代理类来接收返回的结果,代码中使用 getXX 的形式来获取。
在 UserDetailRepository 中添加查询的方法,返回类型设置为 UserInfo:
@Query("select u.userName as userName, u.email as email, d.introduction as introduction , d.hobby as hobby from User u , UserDetail d " +
"where u.id=d.userId and d.hobby = ?1 ")
List findUserInfo(String hobby);
特别注意这里的 SQL 是 HQL,需要写类的名和属性,这块很容易出错。
测试验证:
@Test
public void testUserInfo() {
List userInfos=userDetailRepository.findUserInfo("钓鱼");
for (UserInfo userInfo:userInfos){
System.out.println("userInfo: "+userInfo.getUserName()+"-"+userInfo.getEmail()+"-"+userInfo.getHobby()+"-"+userInfo.getIntroduction());
}
}
运行测试方法后返回:
userInfo: [email protected]钓鱼-程序员
证明关联查询成功,最后的返回结果来自于两个表,按照这个思路可以进行三个或者更多表的关联查询。