SpringBoot环境下QueryDSL-JPA的入门及进阶

阅读本文需要Mysql,Maven和SpringBoot基础知识。


更新日志

  • 2018.03.19更新:增加二、1.2.7 分页的两种写法二、1.2.8 使用Template实现QueryDSL未支持的语法
  • 2018.01.25更新:增加使用心得(查询条件中字段为String时关于null,empty,blank的表达)
  • 2018.01.24更新:增加mysql聚合函数CONCAT,DATE_FORMAT的使用示例

本文由作者三汪首发于。
Demo已上传github

一、环境配置

1. 引入maven依赖

        
        
            com.querydsl
            querydsl-jpa
        
                
            com.querydsl
            querydsl-apt
            provided
        

2. 添加maven插件

添加这个插件是为了让程序自动生成query type(查询实体,命名方式为:"Q"+对应实体名)。
上文引入的依赖中querydsl-apt即是为此插件服务的。

注:在使用过程中,如果遇到query type无法自动生成的情况,用maven更新一下项目即可解决(右键项目->Maven->Update Project)。

            
                com.mysema.maven
                apt-maven-plugin
                1.1.3
                
                    
                        
                            process
                        
                        
                            target/generated-sources/java
                            com.querydsl.apt.jpa.JPAAnnotationProcessor
                        
                    
                
                       

补充:
QueryDSL默认使用HQL发出查询语句。但也支持原生SQL查询。
若要使用原生SQL查询,你需要使用下面这个maven插件生成相应的query type。


  
    
      ...
      
        com.querydsl
        querydsl-maven-plugin
        ${querydsl.version}
        
          
            
              export
            
          
        
        
          org.apache.derby.jdbc.EmbeddedDriver
          jdbc:derby:target/demoDB;create=true
          com.mycompany.mydomain
          ${project.basedir}/target/generated-sources/java
        
        
          
            org.apache.derby
            derby
            ${derby.version}
          
        
      
      ...
    
  

二、使用

在Spring环境下,我们可以通过两种风格来使用QueryDSL。

一种是使用JPAQueryFactory的原生QueryDSL风格,
另一种是基于Spring Data提供的QueryDslPredicateExecutor的Spring-data风格。

使用QueryDslPredicateExecutor可以简化一些代码,使得查询更加优雅。
JPAQueryFactory的优势则体现在其功能的强大,支持更复杂的查询业务。甚至可以用来进行更新和删除操作。

下面分别介绍两种风格的使用方式。

1. JPAQueryFactory

JPAQueryFactory使用逻辑类似于HQL/SQL语法,不再额外说明。
QueryDSL在支持JPA的同时,也提供了对Hibernate的支持。可以通过HibernateQueryFactory来使用。

装配

    @Bean
    @Autowired
    public JPAQueryFactory jpaQuery(EntityManager entityManager) {
        return new JPAQueryFactory(entityManager);
    }

注入

    @Autowired
    JPAQueryFactory queryFactory;

1.1 更新/删除

Update

QMemberDomain qm = QMemberDomain.memberDomain;
queryFactory.update(qm).set(qm.status, "0012").where(qm.status.eq("0011")).execute();

Delete

QMemberDomain qm = QMemberDomain.memberDomain;
queryFactory.delete(qm).where(qm.status.eq("0012")).execute();

1.2 查询

查询简直可以玩出花来。

1.2.1 select()和fetch()的几种常用写法

QMemberDomain qm = QMemberDomain.memberDomain;
//查询字段-select()
List nameList = queryFactory.select(qm.name).from(qm).fetch();
//查询实体-selectFrom()
List memberList = queryFactory.selectFrom(qm).fetch();
//查询并将结果封装至dto中
List dtoList = queryFactory.select(Projections.constructor(MemberFavoriteDto.class,qm.name,qf.favoriteStoreCode)).from(qm).leftJoin(qm.favoriteInfoDomains,qf).fetch();
//去重查询-selectDistinct()
List distinctNameList = queryFactory.selectDistinct(qm.name).from(qm).fetch();
//获取首个查询结果-fetchFirst()
MemberDomain firstMember = queryFactory.selectFrom(qm).fetchFirst();
//获取唯一查询结果-fetchOne()
//当fetchOne()根据查询条件从数据库中查询到多条匹配数据时,会抛`NonUniqueResultException`。
MemberDomain anotherFirstMember = queryFactory.selectFrom(qm).fetchOne();

1.2.2 where子句查询条件的几种常用写法

        //查询条件示例
        List memberConditionList = queryFactory.selectFrom(qm)
                //like示例
                .where(qm.name.like('%'+"Jack"+'%')
                        //contain示例
                        .and(qm.address.contains("厦门"))
                        //equal示例
                        .and(qm.status.eq("0013"))
                        //between
                        .and(qm.age.between(20, 30)))               
                .fetch();

如果你觉得上面的写法不够优雅,我们可以使用QueryDSL提供的BooleanBuilder来进行查询条件管理。
如下

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 memberConditionList = queryFactory.selectFrom(qm).where(builder).fetch();

使用BooleanBuilder,更复杂的查询关系也不怕。
例如

BooleanBuilder builder = new BooleanBuilder();
builder.and(qm.address.contains("厦门"));

BooleanBuilder builder2 = new BooleanBuilder();
builder2.or(qm.status.eq("0013"));
builder2.or(qm.status.eq("0014"));
builder.and(builder2);

List memberComplexConditionList = queryFactory.selectFrom(qm).where(builder).fetch();

1.2.3 多表查询

//以左关联为例-left join
QMemberDomain qm = QMemberDomain.memberDomain;
QFavoriteInfoDomain qf= QFavoriteInfoDomain.favoriteInfoDomain;
List leftJoinList = queryFactory.selectFrom(qm).leftJoin(qm.favoriteInfoDomains,qf).where(qf.favoriteStoreCode.eq("0721")).fetch();

1.2.4 使用Mysql聚合函数

//聚合函数-avg()
Double averageAge = queryFactory.select(qm.age.avg()).from(qm).fetchOne();

//聚合函数-concat()
String concat = queryFactory.select(qm.name.concat(qm.address)).from(qm).fetchOne();

//聚合函数-date_format()
String date = queryFactory.select(Expressions.stringTemplate("DATE_FORMAT({0},'%Y-%m-%d')", qm.registerDate)).from(qm).fetchOne();

当用到DATE_FORMAT这类QueryDSL似乎没有提供支持的Mysql函数时,我们可以手动拼一个String表达式。这样就可以无缝使用Mysql中的函数了。

1.2.5 使用子查询

下面的用法中子查询没有什么实际意义,只是作为一个写法示例。

//子查询
List subList = queryFactory.selectFrom(qm).where(qm.status.in(JPAExpressions.select(qm.status).from(qm))).fetch();

1.2.6 排序

//排序
List orderList = queryFactory.selectFrom(qm).orderBy(qm.name.asc()).fetch();

1.2.7 分页的两种写法

        QMemberDomain qm = QMemberDomain.memberDomain;
        //写法一
        JPAQuery query = queryFactory.selectFrom(qm).orderBy(qm.age.asc());
        long total = query.fetchCount();//hfetchCount的时候上面的orderBy不会被执行
        List list0= query.offset(2).limit(5).fetch();
        //写法二
        QueryResults results = queryFactory.selectFrom(qm).orderBy(qm.age.asc()).offset(2).limit(5).fetchResults();
        List list = results.getResults();
        logger.debug("total:"+results.getTotal());
        logger.debug("limit:"+results.getLimit());
        logger.debug("offset:"+results.getOffset());

写法一和二都会发出两条sql进行查询,一条查询count,一条查询具体数据。
写法二的getTotal()等价于写法一的fetchCount
无论是哪种写法,在查询count的时候,orderBy、limit、offset这三个都不会被执行。可以大胆使用。

1.2.8 使用Template实现QueryDSL未支持的语法

其实Template我们在1.2.4 使用Mysql聚合函数中已经使用过了。QueryDSL并没有对Mysql的所有函数提供支持,好在它给我们提供了Template特性。我们可以使用Template来实现各种QueryDSL未直接支持的语法。
示例如下。

        QMemberDomain qm = QMemberDomain.memberDomain;
        //使用booleanTemplate充当where子句或where子句的一部分
        List list = queryFactory.selectFrom(qm).where(Expressions.booleanTemplate("{} = \"tofu\"", qm.name)).fetch();
        //上面的写法,当booleanTemplate中需要用到多个占位时
        List list1 = queryFactory.selectFrom(qm).where(Expressions.booleanTemplate("{0} = \"tofu\" and {1} = \"Amoy\"", qm.name,qm.address)).fetch();
        
        //使用stringTemplate充当查询语句的某一部分
        String date = queryFactory.select(Expressions.stringTemplate("DATE_FORMAT({0},'%Y-%m-%d')", qm.registerDate)).from(qm).fetchFirst();
        //在where子句中使用stringTemplate
        String id = queryFactory.select(qm.id).from(qm).where(Expressions.stringTemplate("DATE_FORMAT({0},'%Y-%m-%d')", qm.registerDate).eq("2018-03-19")).fetchFirst();

不过Template好用归好用,但也有其局限性。
例如当我们需要用到复杂的正则表达式匹配的时候,就有些捉襟见肘了。这是由于Template中使用了{}来作为占位符,而正则表达式中也可能使用了{},因而会产生冲突。

2. QueryDslPredicateExecutor

我们通常使用Repository来继承QueryDslPredicateExecutor接口。通过注入Repository来使用。

继承

@Repository
public interface IMemberDomainRepository extends JpaRepository,QueryDslPredicateExecutor {

}

注入

@Autowired
IMemberDomainRepository memberRepo;

2.1 查询

简单查询

QMemberDomain qm = QMemberDomain.memberDomain;
Iterable iterable = memberRepo.findAll(qm.status.eq("0013"));

也可以使用更优雅的BooleanBuilder 来进行条件分支管理

BooleanBuilder builder = new BooleanBuilder();
builder.and(qm.address.contains("厦门"));
builder.and(qm.status.eq("0013"));
Iterable iterable2 = memberRepo.findAll(builder);

QueryDslPredicateExecutor接口提供了findOne(),findAll(),count(),exists()四个方法来支持查询。
count()会返回满足查询条件的数据行的数量,exists()会根据所要查询的数据是否存在返回一个boolean值,都很简单,因此不再赘述。
下面着重进行介绍findOne()findAll()两个关键查询方法。

2.1.1 findOne()

findOne,顾名思义,从数据库中查出一条数据。没有重载方法。
JPAQueryfetchOne()一样,当根据查询条件从数据库中查询到多条匹配数据时,会抛NonUniqueResultException。使用的时候需要慎重。

2.1.2 findAll()

findAll是从数据库中查出匹配的所有数据。提供了以下几个重载方法。

  • findAll(Predicate predicate)
  • findAll(OrderSpecifier... orders)
  • findAll(Predicate predicate,OrderSpecifier... orders)
  • findAll(Predicate predicate,Sort sort)

第一个重载方法是不带排序的,第二个重载方法是只带QueryDSL提供的OrderSpecifier方式实现排序而不带查询条件的,而第三个方法则是既有条件又有排序的。
因此我们直接来看第三个方法的使用示例。

QMemberDomain qm = QMemberDomain.memberDomain;
OrderSpecifier order = new OrderSpecifier<>(Order.DESC, qm.age);
Iterable iterable = memberRepo.findAll(qm.status.eq("0013"),order);

除了QueryDSL提供的排序实现,我们还有支持Spring Data提供的Sort的第四个重载方法。示例如下

QMemberDomain qm = QMemberDomain.memberDomain;
Sort sort = new Sort(new Sort.Order(Sort.Direction.ASC, "age"));
Iterable iterable = memberRepo.findAll(qm.status.eq("0013"), sort);

三、使用心得

1. 查询条件中字段为String时关于null,empty,blank的表达

(如果你还不了解null,empty,blank的区别,请先自行搜索了解)
QueryDSL为String类型的字段提供了.isEmpty(),isNull(),.isNotEmpty(),isNotNull()这四个函数支持,唯独没有对blank提供支持。经过测试,我发现可以通过这种方式来实现对blank的使用:.eq(""),.ne("")

四、参考

  • Querydsl Reference Guide
  • QueryDSL通用查询框架学习目录
  • querydsl查询使用函数DATE_FORMAT

五、扩展阅读

  • 【一目了然】Spring Data JPA使用Specification动态构建多表查询、复杂查询及排序示例

以上。
希望我的文章对你能有所帮助。
我不能保证文中所有说法的百分百正确,
但我能保证它们都是我的理解和感悟以及拒绝直接复制黏贴(确实需要引用的部分我会附上源地址)。
有什么意见、见解或疑惑,欢迎留言讨论。

你可能感兴趣的:(SpringBoot环境下QueryDSL-JPA的入门及进阶)