SpringDataJPA系列(5)@Query应该怎么用?
之前说到过,DMQ查询策略有两种:方法命令和@Query注解的方式。为什么需要两种呢?它们分别适用的场景是怎么样的?
@Query使用
定义一个通过名字查询用户的方法
以下是测试方法:
QueryLookupStrategy 实现原理
我们可以通过QueryExecutorMethodInterceptor类来进行跟踪和分析,它是查询方法的拦截器,我们在lookupQuery()方法中打个断点。
可以看到显示默认的策略是CreateIfNotFound,也就是如果有@Query注解,那么以@Query的注解内容为准,可以忽略方法名方式。
我们可以看到strategy.resolveQuery采用了策略模式,它有三种实现策略:
我们可以看到在解析查询的时候,还有个容错机制,出错后还会采用一次方法名个识别方式进行sql语句的拼接
那么接着进入到 llookupStratrgy.resolveQuery 方法里面,我们可以看到图中 ①处,如果 Query 注解找到了,就不会走到 ② 处了。
这时我们点开 Query 里面的 Query 属性的值看一下,你会发现这里同时生成了两个 SQL:一个是查询总数的 Query 定义,另一个是查询结果 Query 定义。
到这里我们已经基本明白了,如果想看看 Query 具体是怎么生成的、上面的 @Param 注解是怎么生效的,可以在上面的图 ① 处 debug 继续往里面看
PS:这里要注意,在Spring启动过的时候,JPA会对资源库的每个方法都进行扫描,然后进行具体查询器RepositoryQuery的选择。
下图是关于RepositoryQuery接口相关类图:
@Query用法和语法
基本语法
@Query 用法是使用 JPQL 为实体创建声明式查询方法。我们一般只需要关心 @Query 里面的 value 和 nativeQuery、countQuery 的值即可,因为其他的不常用。
- value:JPQL表达式
- nativeQuery:JPQL是否是原生的Sql语句
- countQuery :指定count的JPQL语句,如果不指定将根据query自动生成
使用声明式 JPQL 查询有个好处,就是启动的时候就知道你的语法正确不正确。它的语法结构有点类似我们 SQL:
//查询
SELECT ... FROM ...
[WHERE ...]
[GROUP BY ... [HAVING ...]]
[ORDER BY ...]
//删除
DELETE FROM ... [WHERE ...]
//更新
UPDATE ... SET ... [WHERE ...]
你会发现它的语法结构有点类似我们 SQL,唯一的区别就是 JPQL FROM 后面跟的是对象
,而 SQL 里面的字段对应的是对象里面的属性字段
。
其中“…”省略的部分是实体对象名字和实体对象里面的字段名字,而其中类似 SQL 一样包含的语法关键字有:
SELECT FROM WHERE UPDATE DELETE JOIN OUTER INNER LEFT GROUP BY HAVING FETCH DISTINCT OBJECT NULL TRUE FALSE NOT AND OR BETWEEN LIKE IN AS UNKNOWN EMPTY MEMBER OF IS AVG MAX MIN SUM COUNT ORDER BY ASC DESC MOD UPPER LOWER TRIM POSITION CHARACTER_LENGTH CHAR_LENGTH BIT_LENGTH CURRENT_TIME CURRENT_DATE CURRENT_TIMESTAMP NEW EXISTS ALL ANY SOME
用法案例
- 单条件查询
@Query("select u from User u where u.emailAddress = ?1")
User findByEmailAddress(String emailAddress);
- LIKE查询
@Query("select u from User u where u.firstname like %?1")
List<User> findByFirstnameEndsWith(String firstname);
- 原始sql查询,nativeQuery = true 即可,注意nativeQuery 不支持直接 Sort 的参数查询
@Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true)
User findByEmailAddress(String emailAddress);
- nativeQuery 排序的正确写法
@Query(value = "select * from user_info where first_name=?1 order by ?2",nativeQuery = true)
List<UserInfoEntity> findByFirstName(String firstName,String sort);
//调用的地方写法last_name是数据里面的字段名,不是对象的字段名
repository.findByFirstName("jackzhang","last_name");
- JPQL排序
@Query("select u from User u where u.lastname like ?1%")
List<User> findByAndSort(String lastname, Sort sort);
@Query("select u.id, LENGTH(u.firstname) as fn_len from User u where u.lastname like ?1%")
List<Object[]> findByAsArrayAndSort(String lastname, Sort sort);
//调用方的写法,如下:
repo.findByAndSort("lannister", new Sort("firstname"));
repo.findByAndSort("stark", new Sort("LENGTH(firstname)"));
repo.findByAndSort("targaryen", JpaSort.unsafe("LENGTH(firstname)"));
repo.findByAsArrayAndSort("bolton", new Sort("fn_len"));
- JQPl 的排序
@Query(value = "select u from User u where u.lastname = ?1")
Page<User> findByLastname(String lastname, Pageable pageable);
//调用者的写法
repository.findByFirstName("jackzhang",new PageRequest(1,10));
- nativeQuery 的排序
@Query(value = "select * from user_info where first_name=?1 /* #pageable# */",
countQuery = "select count(*) from user_info where first_name=?1",
nativeQuery = true)
Page<UserInfoEntity> findByFirstName(String firstName, Pageable pageable);
}
//调用者的写法
return userRepository.findByFirstName("jackzhang",new PageRequest(1,10, Sort.Direction.DESC,"last_name"));
//打印出来的sql
select * from user_info where first_name=? /* #pageable# */ order by last_name desc limit ?, ?
这里需要注意:这个注释 / #pageable# / 必须有。
- 根据 firstname 和 lastname 参数查询 user 对象
@Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
User findByLastnameOrFirstname(@Param("lastname") String lastname, @Param("firstname") String firstname);
- 根据 firstname 和 lastname 参数查询 user 对象,并带上限制返回
@Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
User findTop10ByLastnameOrFirstname(@Param("lastname") String lastname, @Param("firstname") String firstname);
@Param 注解指定方法参数的具体名称,通过绑定的参数名字指定查询条件,这样不需要关心参数的顺序。比较推荐这种做法,因为它比较利于代码重构
。
@Query最佳实践
使用场景:映射返回指定过的DTO
新增一个实体表
原来的用户表
当我们需要查询用户的名称、部队、主帅技能时应该如何操作?
- 小白写法,查询获得的对象后再塞到DTO中
@Query("select u.name,u.email,e.idCard from User u,UserExtend e where u.id= e.userId and u.id=:id")
List<Object[]> findByUserId(@Param("id") Long id);
- 进阶写法:定义个返回dto,@Query中构建返回dto直接返回
查询方法的实现,注意红色标注部分是实现关键
下面是测试代码:
注意:我们在构建返回的时候还可以使用CONCAT 的关键字做了一个字符串拼接,这对一些统一的返回处理还是有好处的,但不建议太复杂的计算。
我们可以在ParameterizedFunctionExpression 类中看到支持的关键字
- 高阶写法:定义一个返回接口,@Query中构建返回dto直接返回
@Query的查询写法如下:
测试方法如下:
比起 DTO 我们不需要 new 了,并且接口只能读,那么我们返回的结果 DTO 的职责就更单一了,只用来查询。接口的方式是比较推荐的做法,因为它是只读的,对构造方法没有要求,返回的实际是 HashMap。
@Query动态查询
通过上面的实例可以看得出来,我们采用了 :email isnullor s.email = :email 这种方式来实现动态查询的效果,实际工作中也可以演变得很复杂。
总结
- 能用方法名表示的,尽量用方法名表示,因为这样语义清晰、简单快速,基本上只要编译通过,一定不会有问题
- 能用 @Query 里面的 JPQL 表示的,就用 JPQL,这样与 SQL 无关,万一哪天换数据库了,基本上代码不用改变
- 最后实在没有办法了,可以选择 nativeQuery 写原始 SQL,特别是一开始从 MyBatis 转过来的同学,选择写 SQL 会更容易一些