5 Spring Data JPA 之 @Query 语法详解及其应用

5.1 快速体验 @Query 的方法

沿⽤我们之前的例⼦,新增⼀个 @Query 的⽅法:

// 通过 query 注解根据 name 查询 user 信息
@Query("From User where name=:name")
User findByQuery(@Param("name") String nameParam);

新增⼀个测试方法:

@Test
public void testQueryAnnotation() {userRepository.save(User.builder().name("zzn111").email("123456@126.com").sex("man").address("shanghai").build());User user = userRepository.findByQuery("zzn111");System.out.println(user);
}

运行结果如下:

Hibernate: insert into user (address, email, name, sex, id) values (?, ?, ?, ?, ?)
Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.email as email3_0_, user0_.name as name4_0_, user0_.sex as sex5_0_ from user user0_ where user0_.name=?
User(id=1, name=zzn111, email=123456@126.com, sex=man, address=shanghai)

通过上⾯的例⼦可以发现,这次不是通过⽅法名来⽣成查询语法,⽽是 @Query 注解在其中起了作⽤,使 From User where name=:name 这个 JPQL ⽣效了。那么它的实现原理是什么呢?通过源码来看⼀下。

5.2 JpaQueryLookupStrategy 关键源码剖析

我们在之前已经介绍过 QueryLookupStrategy 的策略值有哪些,那么我们来看下源码是如何起作⽤的。

我们先打开 QueryExecutorMethodInterceptor 类,找到如下代码:

再运⾏上⾯的测试⽤例,这时候在这⾥设置⼀个断点,可以看到默认的策略是 CreateIfNotFoundQueryLookupStrategy,也就是如果有 @Query 注解,那么以 @Query 的注解内容为准,可以忽略⽅法名。

我们继续往后⾯看,进⼊到 org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy.DeclaredQueryLookupStrategy#resolveQuery ⾥⾯,如下所示:

我们可以看到 JPA 的判断顺序

  1. 先判断是否定义存储过程,有的话优先使用存储过程并返回
  2. 再判断是否有 Query 注解,如果有的话,再对注解进行处理并返回
  3. 最后再根据方法名生成 SQL
  4. 如果都没有符合条件的话,就抛出异常

5.3 @Query 的基本用法

在讲解它的语法之前,我们看⼀下它的注解源码,了解⼀下基本⽤法。

package org.springframework.data.jpa.repository;public @interface Query {/*** 指定 JPQL 的查询语句。* (nativeQuery=true 的时候,是原⽣的 Sql 语句)*/String value() default "";/*** 指定 count 的 JPQL 语句,如果不指定将根据 query ⾃动⽣成。* (如果当 nativeQuery=true 的时候,指的是原⽣的 Sql 语句)*/String countQuery() default "";/*** 根据哪个字段来 count,⼀般默认即可。*/String countProjection() default "";/*** 默认是 false,表示 value ⾥⾯是不是原⽣的 sql 语句*/boolean nativeQuery() default false;/*** 可以指定⼀个 query 的名字,必须唯⼀的。* 如果不指定,默认的⽣成规则是:* {$domainClass}.${queryMethodName}*/String name() default "";/** 可以指定⼀个 count 的 query 的名字,必须唯⼀的。* 如果不指定,默认的⽣成规则是:* {$domainClass}.${queryMethodName}.count*/String countName() default "";
}

所以到这⾥你会发现, @Query ⽤法是使⽤ JPQL 为实体创建声明式查询⽅法。我们⼀般只需要关⼼ @Query ⾥⾯的 value 和 nativeQuery、countQuery 的值即可,因为其他的不常⽤。

使⽤声明式 JPQL 查询有个好处,就是启动的时候就知道你的语法正确不正确。那么我们简单介绍⼀下 JPQL 语法。

5.3.1 JPQL 的语法

我们先看⼀下查询的语法结构,代码如下:

SELECT ... FROM ...
[WHERE ...]
[GROUP BY ... [HAVING ...]]
[ORDER BY ...]

你会发现它的语法结构有点类似我们 SQL,唯⼀的区别就是 JPQL FROM 后⾯跟的是对象,⽽ SQL ⾥⾯的字段对应的是对象⾥⾯的属性字段。

同理我们看⼀下 update 和 delete 的语法结构:

DELETE FROM ... [WHERE ...]
UPDATE ... SET ... [WHERE ...]

其中“…”省略的部分是实体对象名字和实体对象⾥⾯的字段名字,⽽其中类似 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

Oracle 的⽂档地址:https://docs.oracle.com/html/E13946_04/ejb3_langref.html

5.3.2 @Query 的用法案例

我们通过⼏个案例来了解⼀下 @Query 的⽤法,你就可以知道 @Query 怎么使⽤、怎么传递参数、怎么分⻚等。

案例 1: 要在 Repository 的查询⽅法上声明⼀个注解,这⾥就是 @Query 注解标注的地⽅。

@Query("select u from User u where u.emailAddress = ?1")
User findByEmailAddress(String emailAddress);

案例 2: LIKE 查询,注意 firstname 不会⾃动加上 % 关键字。

@Query("select u from User u where u.firstname like %?1")
List<User> findByFirstnameEndsWith(String firstname);

案例 3: 直接⽤原始 SQL,nativeQuery = true 即可。

@Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true)
User findByEmailAddress(String emailAddress);

注意:nativeQuery 不⽀持直接 Sort 的参数查询。

案例 4: 下⾯是 nativeQuery 的排序错误的写法,会导致⽆法启动。

@Query(value = "select * from user_info where first_name=?1",nativeQuery = true)
List<UserInfoEntity> findByFirstName(String firstName, Sort sort);

案例 5: 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("zzn111","last_name");

通过上⾯⼏个案例,我们看到了 @Query 的⼏种⽤法,你就会明⽩排序、参数、使⽤⽅法、LIKE、原始 SQL 怎么写。下⾯继续通过案例来看下 @Query 的排序。

5.3.3 @Query 的排序

@Query 中在⽤ JPQL 的时候,想要实现排序,⽅法上直接⽤ PageRequest 或者 Sort 参数都可以做到。

在排序实例中,实际使⽤的属性需要与实体模型⾥⾯的字段相匹配,这意味着它们需要解析为查询中使⽤的属性或别名。我们看⼀下例⼦,这是⼀个 state_field_path_expression JPQL 的定义,并且 Sort 的对象⽀持⼀些特定的函数。

案例 6: Sort and JpaSort 的使⽤,它可以进⾏排序。

@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"));

上⾯这个案例讲述的是排序⽤法,再来看下 @Query 的分⻚⽤法。

5.3.4 @Query 的分页

@Query 的分⻚分为两种情况,分别为 JPQL 的排序和 nativeQuery 的排序。看下⾯的案例。

案例 7:直接⽤ Page 对象接受接⼝,参数直接⽤ Pageable 的实现类即可。

@Query(value = "select u from User u where u.lastname = ?1")
Page<User> findByLastname(String lastname, Pageable pageable);
//调⽤者的写法
repository.findByFirstName("zzn111", new PageRequest(1,10));

案例 8:@Query 对原⽣ SQL 的分⻚⽀持,并不是特别友好,因为这种写法⽐较“骇客”,可能随着版本的不同会有所变化。我们以 MySQL 为例。

@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);
//调⽤者的写法
userRepository.findByFirstName("zzn111",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# / 必须有。

另外,随着版本的变化,这个⽅法有可能会进⾏优化。此外还有⼀种实现⽅法,就是⾃⼰写两个查询⽅法,⾃⼰⼿动分⻚。

关于 @Query 的⽤法,还有⼀个需要了解的内容,就是 @ Param ⽤法。

5.3.5 @Param 的用法

@Param 注解指定⽅法参数的具体名称,通过绑定的参数名字指定查询条件,这样不需要关⼼参数的顺序。我⽐较推荐这种做法,因为它⽐较利于代码重构。如果不⽤ @Param 也是可以的,参数是有序的,这使得查询⽅法对参数位置的重构容易出错。我们看个案例。

案例 9:根据 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);

案例 10: 根据参数进⾏查询,top 10 前⾯说的“query method”关键字照样有⽤,如下所示:

@Query("select u from User u where u.firstname = :firstname or u.lastname =:lastname")
User findTop10ByLastnameOrFirstname(@Param("lastname") String lastname, @Param("firstname") String firstname);

这⾥说下我的经验之谈:你在通过 @Query 定义⾃⼰的查询⽅法时,我建议也⽤ Spring Data JPA 的 name query 的命名⽅法,这样下来⻛格就⽐较统⼀了。

上⾯我介绍了 @Query 的基本⽤法,下⾯介绍⼀下 @Query 在我们的实际应⽤中最受欢迎的两处场景。

5.4 @Query 之 Projections 应用返回指定 DTO

5.4.1 利用 UserDto 类

我们在之前的例⼦的基础上新增⼀张表 UserExtend,⾥⾯包含身份证、学号、年龄等信息,最终我们的实体变成如下模样:

@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserExtend { //⽤户扩展信息表@Id@GeneratedValue(strategy= GenerationType.AUTO)private Long id;private Long userId;private String idCard;private Integer ages;private String studentNumber;
}@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User { //⽤户基本信息表
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Long id;
private String name;
private String email;
@Version
private Long version;
private String sex;
private String address;
}

如果我们想定义⼀个 DTO 对象,⾥⾯只要 name、email、idCard,这个时候我们怎么办呢?这种场景⾮常常⻅,但好多⼈使⽤的都不是最佳实践,我在这⾥介绍⼏种⽅式做⼀下对⽐。

我们先看⼀下,刚学 JPA 的时候别⼿别脚的写法:

    /*** 查询⽤户表⾥⾯的name、email和UserExtend表⾥⾯的idCard** @param id* @return*/@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);

我们通过下⾯的测试⽤例来取上⾯ findByUserId ⽅法返回的数据组结果值,再塞到 DTO ⾥⾯,代码如下:

@Test
public void testQueryAnnotation() {// 新增⼀条⽤户数据userDtoRepository.save(User.builder().name("zzn").email("123456@126.com").sex("man").address("shanghai").build());// 再新增⼀条和⽤户⼀对⼀的UserExtend数据userExtendRepository.save(UserExtend.builder().userId(1L).idCard("shengfengzhenghao").ages(18).studentNumber("xuehao001").build());// 查询我们想要的结果List<Object[]> userArray = userDtoRepository.findByUserId(1L);System.out.println(String.valueOf(userArray.get(0)[0]) + String.valueOf(userArray.get(0)[1]));UserDto userDto = UserDto.builder().name(String.valueOf(userArray.get(0)[0])).build();System.out.println(userDto);
}

其实经验的丰富的“⽼司机”⼀看就知道这肯定不是最佳实践,这多麻烦呀,肯定会有更优解。那么我们再对此稍加改造,⽤ UserDto 接收返回结果。

⾸先,我们新建⼀个 UserDto 类的内容。

@Data
@Builder
@AllArgsConstructor
public class UserDto {private String name,email,idCard;
}

其次,我们看下利⽤ @Query 在 Repository ⾥⾯怎么写。

public interface UserDtoRepository extends JpaRepository<User, Long> {@Query("select new com.example.jpa.example1.UserDto(CONCAT(u.name,'JK123'),u.email,e.idCard) from User u,UserExtend e where u.id= e.userId and u.id=:id")UserDto findByUserDtoId(@Param("id") Long id);
}

我们利⽤ JPQL,new 了⼀个 UserDto;再通过构造⽅法,接收查询结果。其中你会发现,我们⽤ CONCAT 的关键字做了⼀个字符串拼接,这时有的同学就会问了,这种⽅法⽀持的关键字有哪些呢?

你可以查看JPQL的 Oracal ⽂档,也可以通过源码来看⽀持的关键字有哪些。

⾸先,我们打开 ParameterizedFunctionExpression 会发现 Hibernate ⽀持的关键字有这么多,都是 MySQL 数据库的查询关键字,这⾥就不⼀⼀解释了。

然后,我们写⼀个测试⽅法,调⽤上⾯的⽅法测试⼀下。

@Test
public void testQueryAnnotationDto() {userDtoRepository.save(User.builder().name("zzn").email("123456@126.com").sex("man").address("shanghai").build());userExtendRepository.save(UserExtend.builder().userId(1L).idCard("shengfengzhenghao").ages(18).studentNumber("xuehao001").build());UserDto userDto = userDtoRepository.findByUserDtoId(1L);System.out.println(userDto);
}

那么还有更简单的⽅法吗?答案是有,下⾯我们利⽤ UserDto 接⼝来实现⼀下。

5.4.2 利用 UserDto 接口

⾸先,新增⼀个 UserSimpleDto 接⼝来得到我们想要的 name、email、idCard 信息。

public interface UserSimpleDto {String getName();String getEmail();String getIdCard();
}

其次,在 UserDtoRepository ⾥⾯新增⼀个⽅法,返回结果是 UserSimpleDto 接⼝。

// 利⽤接⼝ DTO 获得返回结果,需要注意的是每个字段需要 as 和接⼝⾥⾯的 get ⽅法名字保持⼀样
@Query("select CONCAT(u.name,'JK123') as name,UPPER(u.email) as email,e.idCard as idCard from User u,UserExtend e where u.id= e.userId and u.id=:id")
UserSimpleDto findByUserSimpleDtoId(@Param("id") Long id);

然后,测试⽤例写法如下。

@Test
public void testQueryAnnotationDto() {userDtoRepository.save(User.builder().name("zzn").email("123456@126.com").sex("man").address("shanghai").build());userExtendRepository.save(UserExtend.builder().userId(1L).idCard("shengfengzhenghao").ages(18).studentNumber("xuehao001").build());UserSimpleDto userDto = userDtoRepository.findByUserSimpleDtoId(1L);System.out.println(userDto);System.out.println(userDto.getName()+":"+userDto.getEmail()+":"+userDto.getIdCard());
}

我们发现,⽐起 DTO 我们不需要 new 了,并且接⼝只能读,那么我们返回的结果 DTO 的职责就更单⼀了,只⽤来查询。

接⼝的⽅式是我⽐较推荐的做法,因为它是只读的,对构造⽅法没有要求,返回的实际是 HashMap

返回结果介绍完了,那么我们来看下⼀个最常⻅的问题:如何⽤ @Query 注解实现动态查询?

5.5 @Query 动态查询解决方法

我们看⼀个例⼦,来了解⼀下如何实现 @Query 的动态参数查询。

⾸先,新增⼀个 UserOnlyName 接⼝,只查询 User ⾥⾯的 name 和 email 字段。

//获得返回结果
public interface UserOnlyName {String getName();String getEmail();
}

其次,在我们的 UserDtoRepository ⾥⾯新增两个⽅法:⼀个是利⽤ JPQL 实现动态查询,⼀个是利⽤原始 SQL 实现动态查询。

/*** 利⽤JQPl动态查询⽤户信息*/
@Query("select u.name as name,u.email as email from User u where (:name is null or u.name=:name) and(:email is null or u.email=:email)")
UserOnlyName findByUser(@Param("name") String name, @Param("email") String email);/*** 利⽤原始sql动态查询⽤户信息*/
@Query(value = "select u.name as name, u.email as email " +"from user u " +"where (:#{#user.name} is null or u.name = :#{#user.name}) " +"  and (:#{#user.email} is null or u.email =:# {#user.email})",nativeQuery = true)
UserOnlyName findByUser(@Param("user") User user);

然后,我们新增⼀个测试类,测试⼀下上⾯⽅法的结果。

@Test
public void testQueryDinamicDto() {userDtoRepository.save(User.builder().name("zzn").email("123456@126.com").sex("man").address("shanghai").build());UserOnlyName userDto = userDtoRepository.findByUser("zzn", null);System.out.println(userDto.getName() + ":" + userDto.getEmail());UserOnlyName userDto2 =userDtoRepository.findByUser(User.builder().email("123456@126.com").build());System.out.println(userDto2.getName() + ":" + userDto2.getEmail());
}

通过上⾯的实例可以看得出来,我们采⽤了 :email is null or s.email = :email 这种⽅式来实现动态查询的效果,实际⼯作中也可以演变得很复杂。

5.6 本章小结

我们知道定义⽅法名可以获得想要的结果,@Query 注解亦可以获得想要的结果,nativeQuery 也可以获得想要的结果,那么我们该如何做选择呢?

我们一般遵循以下原则:

  1. 能⽤⽅法名表示的,尽量⽤⽅法名表示,因为这样语义清晰、简单快速,基本上只要编译通过,⼀定不会有问题;
  2. 能⽤ @Query ⾥⾯的 JPQL 表示的,就⽤ JPQL,这样与 SQL ⽆关,万⼀哪天换数据库了,基本上代码不⽤改变;
  3. 最后实在没有办法了,可以选择 nativeQuery 写原始 SQL。

好的架构师写代码时报错的顺序是:编译 < 启动 < 运⾏,即越早发现错误越好。

Spring Data JPA 之 @Query 语法详解及其应用相关推荐

  1. java jpa注解哪个包好,Spring Data JPA 中常用注解详解

    一.java对象与数据库字段转化 @Entity:标识实体类是JPA实体,告诉JPA在程序运行时生成实体类对应表 @Table:设置实体类在数据库所对应的表名 @Id:标识类里所在变量为主键 @Gen ...

  2. Spring 之AOP AspectJ切入点语法详解(最全了,不需要再去其他地找了)---zhangkaitao

    Spring 之AOP AspectJ切入点语法详解(最全了,不需要再去其他地找了) http://jinnianshilongnian.iteye.com/blog/1415606    --zha ...

  3. Spring Data Jpa使用@Query时 报错Validation failed for query for method public abstract

    问题:在使用Spring Data Jpa作为持久化层,在使用@Query注解时出现以下错误: Validation failed for query for method public abstra ...

  4. Spring Data JPA REST Query QueryDSL

    案例概述 在本教程中,我们将研究使用Spring Data JPA和Querydsl为REST API构建查询语言. 在本系列的前两篇文章中,我们使用JPA Criteria和Spring Data ...

  5. java 注解 jpa_详解Spring Data JPA使用@Query注解(Using @Query)

    经过几天的折腾,终于到了学习一个重量级的查询方式上,使用@query注解,使用注解有两种方式,一种是jpql的sql语言方式,一种是原生sql的语言,略有区别,后者我们更熟悉一些.话不多说,看代码. ...

  6. 14.spring aop之aspect切入点语法详解

    1.Spring AOP支持的AspectJ切入点指示符 切入点指示符用来指示切入点表达式目的,,在Spring AOP中目前只有执行方法这一个连接点,Spring AOP支持的AspectJ切入点指 ...

  7. 终于有人把Spring Data JPA 讲明白了!

    01 什么是JPA? JPA的全称是 Java Persistence API , 中文的字面意思就是Java 的持久层 API , JPA 就是定义了一系列标准,让实体类和数据库中的表建立一个对应的 ...

  8. Spring data JPA 之 Jackson 在实体里面的注解详解

    8 Spring data JPA 之 Jackson 在实体里面的注解详解 经过前⾯课时的讲解,相信你已经对实体⾥⾯的 JPA 注解有了⼀定的了解,但是实际⼯作中你会发现实体⾥⾯不仅有 JPA 的注 ...

  9. Spring Data JPA 从入门到精通~Naming命名策略详解及其实践

    Naming 命名策略详解及其实践 用 JPA 离不开 @Entity 实体,我都知道实体里面有字段映射,而字段映射的方法有两种: 显式命名:在映射配置时,设置的数据库表名.列名等,就是进行显式命名, ...

最新文章

  1. 炼个BERT别人花几分钟你花了快1天?谷歌:我这是4810亿参数的巨型BERT
  2. 深度学习中的噪声数据
  3. oracle 11g asm 磁盘组兼容属性
  4. 【企业管理】组织与管理的思考
  5. Hive内部表与外部表区别详细介绍
  6. 汉语拼音/pinyin4j
  7. 关于IE透明度失效的问题
  8. oracle报错12516,Oracle连接数太多报错-ORA-12516异常
  9. java 自然常数e中出现的连续的第一个10个数字组成的质数_自然常数-常数e的来历e在很多数学公式中出现的频率比较高今天做导数题时看到 爱问知识人...
  10. 点播和播放器下载需要的参数的区别(VideoId、AccessKeyId、AccessKeySecret、playKey、playauth)...
  11. efficientnet
  12. iphone7一晚上掉电50%_苹果7待机一晚掉电多少
  13. /*深度优先建立深林,孩子兄弟法*/
  14. 纸壳CMS主题增强,支持主题中加入模板
  15. H3C服务器修改启动项,H3C服务器 iFIST快速安装指南-6W102
  16. 让nodeJS支持ES6的词法----babel的安装和使用
  17. Android-记账本(一)-效果图
  18. IDEA中展开包结构的方法
  19. 如何打造个人品牌(IP)?
  20. 如何解决磁盘坏道的问题

热门文章

  1. H264 demux后AVPacket送去decode时出错
  2. JPA开发(下)一(多)对多的配置与crud操作
  3. indesign中调出字符样式快捷键_InDesign快捷键大全 InDesign常用快捷键分享
  4. 作文 深海机器人_海底机器人作文500字
  5. 一个简单的审批流程系统设计
  6. 六度分离 ( floyd )
  7. matlab 两幅图求并集,MATLAB交并集运算
  8. Android Wallpaper之设置壁纸流程
  9. 原型链----看懂_proto_和prototype
  10. x-ray图像增强算法