Querydsl JPA 基础入门

1,248 阅读6分钟

前言

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…

以测试用例形式提供各类例子,运行结果如下:

image-20220220225919432