QueryDSL——比SpringData JPA更面向对象

8,742 阅读3分钟

SpringData JPA 大家应该已经比较熟悉了,它简化了 JPA 的方法, 使得我们可以快速的操作数据库。在一些复杂的业务中,比如分页、条件查询等,SpringData JPA 为我们封装了一些列的类和方法(SpecificationPageable),不需要写 SQL,以一种面向对象的方式进行数据的查询。

QueryDSL 则是在 SpringData JPA 的基础上,更近一步,提供了统一的类型安全的查询方式,正如标题所说的,它更面向对象了。QueryDSL 由一些查询方法构成,熟悉 SpringData JPA 基本上可以立刻上手去 coding。本文就讲讲它的一些用法,已经部分由于 SpringData JPA 的查询方式。

引入

QueryDSL 有多个模块,可以支持 JPA、Mongodb、Lucene 等,这里我们用到的 module 是 JPA。

<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
</dependency>

<!-- 代码生成插件配置 -->
<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>
</plugin>

为了满足 QueryDSL 查询的规范,我们需要为每一个持久化类生成一个对应的 EntityPath<T>,这个过程可通过 maven 插件自动生成。添加该插件,执行 maven compile 后,会在 entity 同名的包下生成 Q 开头的类。

配置

使用 QueryDSL 有两种方式。

  • 一种是注入一个 JPAQueryFactory 对象,通过它来执行各种查询操作,可类比 JdbcTemplate
@Configuration
public class QuerydslConfig {
    @Bean
    public JPAQueryFactory jpaQuery(EntityManager entityManager) {
        return new JPAQueryFactory(entityManager);
    }
}
  • 另一种是让 repository 实现实现 QuerydslPredicateExecutor<T> 接口,可类比 JpaSpecificationExecutor<T> 的用法。
@NoRepositoryBean
public interface IBaseRepository<T, PK extends Serializable> extends
        JpaRepository<T, PK>, JpaSpecificationExecutor<T>, QuerydslPredicateExecutor<T> {
}

用法

// 基本用法
QCompany qc = QCompany.company;
queryFactory.update(qc).where(qc.id.eq(1)).set(qc.createdTime, LocalDateTime.now()).execute();
queryFactory.delete(qc).where(qc.createdTime.after(LocalDateTime.now().minusDays(5))).execute();
Company c = queryFactory.selectFrom(qc)
            .where(qc.createdTime.eq(LocalDateTime.now())
                   .and(qc.companyName.like("%aa%"))).fetchOne();

QueryDSL 支持丰富的查询方法,具体可以 参考最后的资料。基本的操作如上,可以看到一些 sql 中的操作均通过 java 方法的形式进行调用,可以时说是将面向对象进行到底了。下面部分例子来自博客 《SpringBoot 环境下 QueryDSL-JPA 的入门及进阶》

查询条件

List<MemberDomain> memberConditionList = queryFactory.selectFrom(qm)
    .where(qm.name.like('%'+"Jack"+'%')
           .and(qm.address.contains("厦门"))
           .and(qm.status.eq("0013"))
           .and(qm.age.between(20, 30)))
    	   .and(qm.state.in(1,2))
    .fetch();

结果封装

QMemberDomain qm = QMemberDomain.memberDomain;
// 查询字段 - select()
List<String> nameList = queryFactory.select(qm.name).from(qm).fetch();

// 查询实体 - selectFrom()
List<MemberDomain> memberList = queryFactory.selectFrom(qm).fetch();

// 查询并将结果封装至 dto 中
List<MemberVo> dtoList = queryFactory.select(Projections.constructor(MemberVo.class,qm.name,qm.address)).from(qm).fetch();

控制查询结果的条数

// 去重查询 - selectDistinct()
List<String> distinctNameList = queryFactory.selectDistinct(qm.name).from(qm).fetch();

// 获取首个查询结果 - fetchFirst()
MemberDomain firstMember = queryFactory.selectFrom(qm).fetchFirst();

// 获取唯一查询结果 - fetchOne()
// 当 fetchOne() 根据查询条件从数据库中查询到多条匹配数据时,会抛 `NonUniqueResultException`。
MemberDomain anotherFirstMember = queryFactory.selectFrom(qm).fetchOne();

条件查询 + 投影

条件查询在 JPA 中一般需要自己拼接 sql,或者使用 Specification。而 Specification 只能将所有数据都查出来,无法实现投影。那么在 QueryDSL 中就灵活很多,而且代码也比较容易理解。

BooleanBuilder builder = new BooleanBuilder();
//like
builder.and(qm.name.like('%'+"Jack"+'%'));
//contain
builder.and(qm.address.contains("厦门"));
//equal 示例
builder.and(qm.status.eq("0013"));
//between
builder.and(qm.age.between(20, 30));

List<MemberVo> dtoList = queryFactory.select(Projections.constructor(MemberVo.class,qm.name,qm.address))
    .from(qm).where(builder).fetch();

只需要构建一个 BooleanBuilder 就可以了,相比之前的使用 Specification 而写的匿名内部类要简单很多。

其他查询

// 左连接
QTransaction qt = QTransaction.transaction;
QContract qc = QContract.contract1;
List<Transaction> list = queryFactory.selectFrom(qt).leftJoin(qc).on(qt.contractId.eq(qc.id)).fetch();

// 子查询
QOrgInfo qo = QOrgInfo.orgInfo;
QOrgUser qou = QOrgUser.orgUser;
List<String> list = jpaQueryHZ.select(qo.leaderEmail).from(qo)
    .where(qo.id.in(JPAExpressions.select(qou.tid).from(qou).where(qou.email.in(members))))
    .fetch();

排序、分页

QMeeting qm = QMeeting.meeting;
Pageable pageable = PageRequest.of(param.getPageIndex() - 1, param.getPageSize());
QueryResults<Meeting> results = jpaQueryHZ.selectFrom(qm).where(builder)
    .orderBy(qm.meetingDate.desc())
    .orderBy(qm.meetingStart.desc())
    .offset(pageable.getOffset()).limit(pageable.getPageSize())
    .fetchResults();
List<MeetingVO> list = results.getResults();

总结

本文主要简单介绍了一下 QueryDSL 的一些用法,他可以搭配 JPA 一起使用,在可读性和编码效率上能有不少的提高,尤其是在一些相对比较复杂的查询下,例如条件查询,表连接等。

参考文献