第 3-5 课:Spring Data JPA 的⾼级⽤法

上节课介绍了 Spring Data JPA 的使⽤⽅式和基本查询,常⽤的增、删、改、查需求 Spring Data JPA 已经实
现了。但对于复杂的数据库场景,动态⽣成⽅法不能满⾜,对此 Spring Data JPA 提供了其他的解决⽅案,
这就是这节课的主要内容。

⾃定义 SQL 查询

使⽤ Spring Data ⼤部分的 SQL 都可以根据⽅法名定义的⽅式来实现,但是由于某些原因必须使⽤⾃定义的
SQL 来查询, Spring Data 也可以完美⽀持。
 
SQL 的查询⽅法上⾯使⽤ @Query 注解,在注解内写 Hql 来查询内容。
 
@Query("select u from User u")
Page<User> findALL(Pageable pageable);
当然如果感觉使⽤原⽣ SQL 更习惯,它也是⽀持的,需要再添加⼀个参数 nativeQuery = true
@Query("select * from user u where u.nick_name = ?1", nativeQuery = true)
Page<User> findByNickName(String nickName, Pageable pageable);
@Query 上⾯的 1 代表的是⽅法参数⾥⾯的顺序,如果有多个参数也可以按照这个⽅式添加 1 2 3.... 。除
了按照这种⽅式传参外,还可以使⽤ @Param 来⽀持。
@Query("select u from User u where u.nickName = :nickName")
Page<User> 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 wh
ere u.passWord = ?1"),
 @NamedQuery(name = "User.findByNickName", query = "select u from User u wh
ere u.nickName = ?1"),
})
public class User {
 ……
}
通过 @NamedQueries 注解可以定义多个命名 Query @NamedQuery name 属性定义了 Query 的名
称,注意加上 Entity 名称 . 作为前缀, query 属性定义查询语句。
定义对应的⽅法:
List<User> findByPassWord(String passWord);
List<User> findByNickName(String nickName);

 

Query 查找策略

到此,我们有了三种⽅法来定义 Query :( 1 )通过⽅法名⾃动创建 Query ,( 2 )通过 @Query 注解实现⾃
定义 Query ,( 3 )通过 @NamedQuery 注解来定义 Query 。那么, Spring Data JPA 如何来查找这些 Query
?
通过配置 @EnableJpaRepositories queryLookupStrategy 属性来配置 Query 查找策略,有如下定义。
  • CREATE:尝试从查询⽅法名构造特定于存储的查询。⼀般的⽅法是从⽅法名中删除⼀组已知的前缀, 并解析⽅法的其余部分。
  • USE_DECLARED_QUERY:尝试查找已声明的查询,如果找不到,则抛出异常。查询可以通过某个地 ⽅的注释定义,也可以通过其他⽅式声明。
  • CREATE_IFNOTFOUND(默认):CREATE USE_DECLARED_QUERY 的组合,它⾸先查找⼀个 已声明的查询,如果没有找到已声明的查询,它将创建⼀个⾃定义⽅法基于名称的查询。它允许通过⽅ 法名进⾏快速查询定义,还可以根据需要引⼊声明的查询来定制这些查询调优。
⼀般情况下使⽤默认配置即可,如果确定项⽬ Query 的具体定义⽅式,可以更改上述配置,例如,全部使⽤
@Query 来定义查询,⼜或者全部使⽤命名的查询。
 

分⻚查询

Spring Data JPA 已经帮我们内置了分⻚功能,在查询的⽅法中,需要传⼊参数 Pageable ,当查询中有多个

参数的时候 Pageable 建议作为最后⼀个参数传⼊。

@Query("select u from User u")
Page<User> findALL(Pageable pageable);
Page<User> findByNickName(String nickName, Pageable pageable);
Pageable Spring 封装的分⻚实现类,使⽤的时候需要传⼊⻚数、每⻚条数和排序规则, Page Spring
封装的分⻚对象,封装了总⻚数、分⻚数据等。返回对象除使⽤ Page 外,还可以使⽤ Slice 作为返回值。
 
Slice<User> 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<User> queryFirst10ByLastname(String lastname, Pageable pageable);
List<User> findFirst10ByLastname(String lastname, Sort sort);
List<User> findTop10ByLastname(String lastname, Pageable pageable);

复杂查询

我们可以通过 AND 或者 OR 等连接词来不断拼接属性来构建多条件查询,但如果参数⼤于 6 个时,⽅法名
就会变得⾮常的⻓,并且还不能解决动态多条件查询的场景。到这⾥就需要给⼤家介绍另外⼀个利器
JpaSpecifificationExecutor 了。
 
JpaSpecifificationExecutor JPA 2.0 提供的 Criteria API 的使⽤封装,可以⽤于动态⽣成 Query 来满⾜我们
业务中的各种复杂场景。 Spring Data JPA 为我们提供了 JpaSpecifificationExecutor 接⼝,只要简单实现
toPredicate ⽅法就可以实现复杂的查询。
 
我们来看⼀下 JpaSpecifificationExecutor 的源码:
public interface JpaSpecificationExecutor<T> {
 //根据 Specification 条件查询单个对象,注意的是,如果条件能查出来多个会报错
 T findOne(@Nullable Specification<T> spec);
 //根据 Specification 条件查询 List 结果
 List<T> findAll(@Nullable Specification<T> spec);
 //根据 Specification 条件,分⻚查询
 Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);
 //根据 Specification 条件,带排序的查询结果
 List<T> findAll(@Nullable Specification<T> spec, Sort sort);
 //根据 Specification 条件,查询数量
 long count(@Nullable Specification<T> spec);
}
JpaSpecifificationExecutor 的源码很简单,根据 Specifification 的查询条件返回 List Page 或者 count 数据。
在使⽤ JpaSpecifificationExecutor 构建复杂查询场景之前,我们需要了解⼏个概念:
  • Root<T> root,代表了可以查询和操作的实体对象的根,开⼀个通过 get("属性名") 来获取对应的值。
  • CriteriaQuery<?> query,代表⼀个 specifific 的顶层查询对象,它包含着查询的各个部分,⽐如 select fromwheregroup byorder by 等。
  • CriteriaBuilder cb,来构建 CritiaQuery 的构建器对象,其实就相当于条件或者是条件组合,并以 Predicate 的形式返回。
使⽤案例
 
下⾯的使⽤案例中会报错这⼏个对象的使⽤。
⾸先定义⼀个 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<UserDetail>
,JpaRepository<UserDetail, Long> {
}
定义⼀个查询 Page<UserDetail> 的接⼝:
public interface UserDetailService {
 public Page<UserDetail> findByCondition(UserDetailParam detailParam, Pageable 
pageable);
}
UserDetailServiceImpl 中,我们来演示 JpaSpecifificationExecutor 的具体使⽤。
@Service
public class UserDetailServiceImpl implements UserDetailService{
 @Resource
 private UserDetailRepository userDetailRepository;
 @Override
 public Page<UserDetail> findByCondition(UserDetailParam detailParam, Pageable 
pageable){
 return userDetailRepository.findAll((root, query, cb) -> {
 List<Predicate> predicates = new ArrayList<Predicate>();
 //equal 示例
 if (!StringUtils.isNullOrEmpty(detailParam.getIntroduction())){
 predicates.add(cb.equal(root.get("introduction"),detailParam.getIn
troduction()));
 }
 //like 示例
 if (!StringUtils.isNullOrEmpty(detailParam.getRealName())){
 predicates.add(cb.like(root.get("realName"),"%"+detailParam.getRea
lName()+"%"));
 }
 //between 示例
 if (detailParam.getMinAge()!=null && detailParam.getMaxAge()!=null) {
 Predicate agePredicate = cb.between(root.get("age"), detailParam.g
etMinAge(), detailParam.getMaxAge());
 predicates.add(agePredicate);
 }
 //greaterThan ⼤于等于示例
 if (detailParam.getMinAge()!=null){
 predicates.add(cb.greaterThan(root.get("age"),detailParam.getMinAg
e()));
 }
 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<UserDetail> 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 introdu
ction , d.hobby as hobby from User u , UserDetail d " +
 "where u.id=d.userId and d.hobby = ?1 ")
List<UserInfo> findUserInfo(String hobby);
特别注意这⾥的 SQL HQL ,需要写类的名和属性,这块很容易出错。
 
测试验证:
 
 
@Test
public void testUserInfo() {
 List<UserInfo> userInfos=userDetailRepository.findUserInfo("钓⻥");
 for (UserInfo userInfo:userInfos){
 System.out.println("userInfo: "+userInfo.getUserName()+"-"+userInfo.getEma
il()+"-"+userInfo.getHobby()+"-"+userInfo.getIntroduction());
 }
}
运⾏测试⽅法后返回:
userInfo: aa-aa@126.com-钓⻥-程序员
证明关联查询成功,最后的返回结果来⾃于两个表,按照这个思路可以进⾏三个或者更多表的关联查询。

总结

Spring Data JPA 使⽤动态注⼊的原理,根据⽅法名动态⽣成⽅法的实现,因此根据⽅法名实现数据查询,即
可满⾜⽇常绝⼤部分使⽤场景。除了这种查询⽅式之外, Spring Data JPA 还⽀持多种⾃定义查询来满⾜更多
复杂场景的使⽤,两种⽅式相结合可以灵活满⾜项⽬对 Orm 层的需求。
 
通过学习 Spring Data JPA 也可以看出 Spring Boot 的设计思想, 80% 的需求通过默认、简单的⽅式实现,
满⾜⼤部分使⽤场景,对于另外 20% 复杂的场景,提供另外的技术⼿段来解决。 Spring Data JPA 中根据⽅
法名动态实现 SQL ,组件环境⾃动配置等细节,都是将 Spring Boot 约定优于配置 的思想体现的淋淋尽致。
 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值