上节课介绍了
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 、from、where、group by、order 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
约定优于配置
的思想体现的淋淋尽致。