前言
Querydsl 是一个可以构建静态类型的类SQL查询框架,无需将查询编写为内联字符串或将它们外部化为 XML 文件,它们可以通过 Querydsl 之类的流畅 API 构建。与简单字符串相比,使用流畅的 API 的好处是:
- IDE中的代码补全
- 几乎不允许任何语法无效的查询
- 可以安全地引用域类型和属性
- 较好地用于重构域类型中的更改
简介
背景
Querydsl 的诞生是出于以类型安全的方式维护 HQL 查询的需要。HQL 查询的增量构造需要字符串连接,导致代码难以阅读。通过纯字符串对域类型和属性的不安全引用是基于字符串的 HQL 构造的另一个问题。
随着领域模型的变化,类型安全为软件开发带来了巨大的好处。域更改直接反映在查询中,查询构造中的自动完成使查询构造更快、更安全。
HQL for Hibernate 是 Querydsl 的第一个目标语言,但现在它支持 JPA、JDO、JDBC、Lucene、Hibernate Search、MongoDB、Collections 和 RDFBean 作为后端。
原则
类型安全 是 Querydsl 的核心原则。查询是根据生成的查询类型构建的,这些查询类型反映了您的域类型的属性。函数/方法调用也以完全类型安全的方式构造。
一致性 是另一个重要原则。查询路径和操作在所有实现中都是相同的,查询接口也有一个通用的基本接口。
Querydsl JPA 引入
引入maven依赖
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>${querydsl.version}</version>
</dependency>
添加maven插件
<project>
<build>
<plugins>
...
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
</dependency>
</dependencies>
</plugin>
...
</plugins>
</build>
</project>
版本号说明(querydsl.version)
对于springboot项目,常用依赖的版本号是通过 spring-boot-starter-parent 进行管理。配置继承 spring-boot-starter-parent 并指定 springboot版本号,之后导入其他starter或部分第三方依赖,可以省略版本号
继承 spring-boot-starter-parent
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
spring-boot-starter-parent 继承自 spring-boot-dependencies,实际的依赖项版本控制,及依赖的声明都是在 spring-boot-dependencies 进行的
spring-boot-dependencies-2.6.3.pom部分代码
...
<properties>
<activemq.version>5.16.3</activemq.version>
...
<querydsl.version>5.0.0</querydsl.version>
...
<xmlunit2.version>2.8.4</xmlunit2.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-amqp</artifactId>
<version>${activemq.version}</version>
</dependency>
...
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-bom</artifactId>
<version>${querydsl.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
...
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-bom</artifactId>
<version>${spring-session-bom.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Querydsl JPA 实例
删改查
查询
单表查询
java代码
jpaQueryFactory.selectFrom(qUserDO).fetch();
生成sql
SELECT userdo0_.id AS id1_1_, userdo0_.dept_id AS dept_id2_1_, userdo0_.name AS name3_1_, userdo0_.username AS username4_1_
FROM t_user userdo0_
联表查询-cross join (from)
java代码
jpaQueryFactory
.select(qUserDO)
.from(qUserDO, qDeptDO)
.where(qUserDO.deptId.eq(qDeptDO.id))
.fetch();
生成sql
SELECT userdo0_.id AS id1_1_, userdo0_.dept_id AS dept_id2_1_, userdo0_.name AS name3_1_, userdo0_.username AS username4_1_
FROM t_user userdo0_
CROSS JOIN t_dept deptdo1_
WHERE userdo0_.dept_id = deptdo1_.id
条件查询-eq
java代码
jpaQueryFactory
.selectFrom(qUserDO)
.where(qUserDO.id.eq(1L))
.fetch();
生成sql
SELECT userdo0_.id AS id1_1_, userdo0_.dept_id AS dept_id2_1_, userdo0_.name AS name3_1_, userdo0_.username AS username4_1_
FROM t_user userdo0_
WHERE userdo0_.id = 1
条件查询-like
java代码
jpaQueryFactory
.selectFrom(qDeptDO)
.where(qDeptDO.name.like("开发" + "%"))
.fetch();
生成sql
SELECT deptdo0_.id AS id1_0_, deptdo0_.name AS name2_0_, deptdo0_.sort AS sort3_0_
FROM t_dept deptdo0_
WHERE deptdo0_.name LIKE '开发%' ESCAPE '!'
条件查询-startWith
java代码
jpaQueryFactory
.selectFrom(qDeptDO)
.where(qDeptDO.name.startsWith("开发"))
.fetch();
生成sql
SELECT deptdo0_.id AS id1_0_, deptdo0_.name AS name2_0_, deptdo0_.sort AS sort3_0_
FROM t_dept deptdo0_
WHERE deptdo0_.name LIKE '开发%' ESCAPE '!'
条件查询-endsWith
java代码
jpaQueryFactory
.selectFrom(qDeptDO)
.where(qDeptDO.name.endsWith("一部"))
.fetch();
生成sql
SELECT deptdo0_.id AS id1_0_, deptdo0_.name AS name2_0_, deptdo0_.sort AS sort3_0_
FROM t_dept deptdo0_
WHERE deptdo0_.name LIKE '%一部' ESCAPE '!'
子查询
要创建子查询,需要使用JPAExpressions的静态工厂方法,并通过from, where等定义查询参数 。
java代码
List<DeptDO> deptDOList = jpaQueryFactory.selectFrom(qDeptDO)
.where(qDeptDO.id.eq(
JPAExpressions.select(qDeptDO.id.max()).from(qDeptDO)
)).fetch();
Assertions.assertEquals(deptDOList.size(), 1);
生成sql
SELECT deptdo0_.id AS id1_0_, deptdo0_.name AS name2_0_, deptdo0_.sort AS sort3_0_
FROM t_dept deptdo0_
WHERE deptdo0_.id = (
SELECT MAX(deptdo1_.id)
FROM t_dept deptdo1_
)
更新
Querydsl JPA中的删除子句遵循简单的update-set/where-execute形式,例子如下:
java代码
Long num = jpaQueryFactory.update(qDeptDO).set(qDeptDO.name, "更新值").where(qDeptDO.id.eq(1L)).execute();
Assertions.assertEquals(num, 1L);
生成sql
update t_dept set name='更新值' where id=1
set调用以SQL-Update样式定义属性更新,execute调用执行Update并返回已更新实体的数量。
JPA中的DML语法没有考虑JPA级联规则,也没有提供细粒度的二级缓存交互。
删除
Querydsl JPA中的删除子句遵循简单的Delete -where-execute形式,例子如下:
java代码
Long num = jpaQueryFactory.delete(qDeptDO).where(qDeptDO.id.eq(1L)).execute();
Assertions.assertEquals(num, 1L);
生成sql
delete from t_dept where id=1
where调用是可选的,execute调用执行删除操作并返回已删除实体的数量。
JPA中的DML语法没有考虑JPA级联规则,也没有提供细粒度的二级缓存交互。(同update)
结果处理
返回多列(Returning multiple columns)
从querydsl 3.0开始,多列结果的默认类型是com.querydsl.core.tuple。元组提供一种类型安全的映射,就像从元组对象访问部分列数据的接口。
java代码
// 查询
List<Tuple> result = jpaQueryFactory.select(qDeptDO.name, qDeptDO.sort).from(qDeptDO).fetch();
// 结果处理
for (Tuple row : result) {
System.out.println("name " + row.get(qDeptDO.name));
System.out.println("sort " + row.get(qDeptDO.sort));
}}
生成sql
select deptdo0_.name as col_0_0_, deptdo0_.sort as col_1_0_
from t_dept deptdo0_
对象填充(Bean population)
需要根据查询结果填充对象的时候,可以使用以下两种方法进行
Projections.bean
Projections.bean需要待填充对象的属性有 set方法,通过set进行属性的填充
java代码
List<DeptWithSetterDTO> result = jpaQueryFactory
.select(Projections.bean(DeptWithSetterDTO.class, qDeptDO.name, qDeptDO.sort))
.from(qDeptDO).fetch();
生成sql
select deptdo0_.name as col_0_0_, deptdo0_.sort as col_1_0_
from t_dept deptdo0_
Projections.fields
Projections.fields适用于待填充对象的属性没有set 方法的情景,通过反射进行属性的注入
java代码
public class DeptNoSetterDTO {
private String name;
private Integer sort;
}
List<DeptNoSetterDTO> result = jpaQueryFactory
.select(Projections.fields(DeptNoSetterDTO.class, qDeptDO.name, qDeptDO.sort))
.from(qDeptDO).fetch();
生成sql
select deptdo0_.name as col_0_0_, deptdo0_.sort as col_1_0_
from t_dept deptdo0_
使用构造函数(Constructor usage)
使用构造函数的方式比较多,既可以在entity类中使用,也可以在DTO类中使用,例子如下:
Projections.constructor
Projections.constructor使用的是类原始的构造函数
public class DeptConstructorDTO {
private String name;
private Integer sort;
public DeptConstructorDTO(String name, Integer sort) {
this.name = name;
this.sort = sort;
}
}
// 查询
List<DeptConstructorDTO> result = jpaQueryFactory
.select(Projections.constructor(DeptConstructorDTO.class, qDeptDO.name, qDeptDO.sort))
.from(qDeptDO).fetch();
生成sql
SELECT deptdo0_.name AS col_0_0_, deptdo0_.sort AS col_1_0_
FROM t_dept deptdo0_
注解@QueryProjection
@QueryProjection是普通构造函数的一个替代方案,与Projections.constructor使用上的不同,只是将其替换为编译生成的查询类型(Q**类)中的构造函数,如果QueryProjection注释的类型不是一个带注释的实体类型(DTO),使用new方式;如果注释类型是一个实体类型,需要通过调用查询类型的静态方法创建,如下:
DTO中使用(无@Entity)
使用new
public class DeptConstructorUseAnnotationDTO {
private String name;
private Integer sort;
@QueryProjection
public DeptConstructorUseAnnotationDTO(String name, Integer sort) {
this.name = name;
this.sort = sort;
}
}
// 查询
List<DeptConstructorUseAnnotationDTO> result = jpaQueryFactory
.select(new QDeptConstructorUseAnnotationDTO(qDeptDO.name, qDeptDO.sort))
.from(qDeptDO).fetch();
实体类中使用(有@Entity)
使用静态方法 create
@Entity
public class DeptDO {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
private String name;
private Integer sort;
public DeptDO() {
}
@QueryProjection
public DeptDO(String name, Integer sort) {
this.name = name;
this.sort = sort;
}
}
// 查询
List<DeptDO> result1 = jpaQueryFactory
.select(QDeptDO.create(qDeptDO.name, qDeptDO.sort))
.from(qDeptDO).fetch();
结果聚合
com.querydsl.core.group.GroupBy
类提供了聚合功能,可用于在内存中聚合查询结果。可以返回List、Set、Map以级 Group类型的聚合结果
聚合返回 List
java代码
// 查询
Map<Long, List<UserDO>> listMap = jpaQueryFactory
.from(qDeptDO, qUserDO)
.where(qDeptDO.id.eq(qUserDO.deptId))
.transform(GroupBy.groupBy(qDeptDO.id).as(GroupBy.list(qUserDO)));
// 结果处理
Assertions.assertEquals(listMap.get(1L).size(), 2);
Assertions.assertEquals(listMap.get(2L).size(), 2);
生成sql
SELECT deptdo0_.id, userdo1_.id, userdo1_.id, userdo1_.dept_id, userdo1_.name, userdo1_.username
FROM t_dept deptdo0_
CROSS JOIN t_user userdo1_
WHERE deptdo0_.id = userdo1_.dept_id
聚合返回 Map
java代码
// 查询
Map<Long, Group> listMap = jpaQueryFactory
.from(qDeptDO, qUserDO)
.where(qDeptDO.id.eq(qUserDO.deptId))
.transform(GroupBy.groupBy(qDeptDO.id).as(qUserDO.name, GroupBy.map(qUserDO.name, qUserDO.username)));
// 结果处理
Assertions.assertEquals(listMap.get(1L).getOne(qUserDO.name), "张三");
Assertions.assertEquals(listMap.get(1L).getMap(qUserDO.name, qUserDO.username).size(), 2);
生成sql
SELECT deptdo0_.id, userdo1_.name, userdo1_.name, userdo1_.username
FROM t_dept deptdo0_
CROSS JOIN t_user userdo1_
WHERE deptdo0_.id = userdo1_.dept_id
聚合返回 Group
Group属于更高一级的集合,可以包含List、Set、Map等
java代码
// 查询
Map<Long, Group> listMap = jpaQueryFactory
.from(qDeptDO, qUserDO)
.where(qDeptDO.id.eq(qUserDO.deptId))
.transform(
GroupBy.groupBy(qDeptDO.id)
.as(qUserDO.name, GroupBy.list(qUserDO.id), GroupBy.set(qUserDO.username))
);
// 结果处理
Assertions.assertEquals(listMap.get(1L).getOne(qUserDO.name), "张三");
Assertions.assertEquals(listMap.get(2L).getList(qUserDO.id), Arrays.asList(3L, 4L));
Assertions.assertEquals(listMap.get(2L).getSet(qUserDO.username).size(), 2);
生成sql
SELECT deptdo0_.id, userdo1_.name, userdo1_.id, userdo1_.username
FROM t_dept deptdo0_
CROSS JOIN t_user userdo1_
WHERE deptdo0_.id = userdo1_.dept_id
聚合返回 对象(bean)
此种聚合适用于 一对一,一对多返回实体列表时的对象聚合。以部门、用户为例,部门:用户
为 1:n
。部门、用户联合查询时,按主键获取部门对象,例子如下:
java代码
// 查询
Map<Long, DeptDO> listMap = jpaQueryFactory
.from(qDeptDO, qUserDO)
.where(qDeptDO.id.eq(qUserDO.deptId))
.transform(GroupBy.groupBy(qUserDO.deptId).as(
Projections.bean(DeptDO.class, qDeptDO.id, qDeptDO.name, qDeptDO.sort)
));
// 结果处理
DeptDO deptDO = listMap.get(1L);
Assertions.assertNotNull(deptDO);
Assertions.assertEquals(1L, deptDO.getId());
生成sql
SELECT userdo1_.dept_id, deptdo0_.id, deptdo0_.name, deptdo0_.sort
FROM t_dept deptdo0_
CROSS JOIN t_user userdo1_
WHERE deptdo0_.id = userdo1_.dept_id
实例代码地址
gitHub地址: github.com/wangxd-yu/y…
以测试用例形式提供各类例子,运行结果如下: