美文网首页Spring BootJava服务器端编程程序员
SpringBoot环境下QueryDSL-JPA的入门及进阶

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

作者: 三汪 | 来源:发表于2017-12-21 09:51 被阅读1349次

    阅读本文需要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依赖

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

    2. 添加maven插件

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

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

                <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默认使用HQL发出查询语句。但也支持原生SQL查询。
    若要使用原生SQL查询,你需要使用下面这个maven插件生成相应的query type。

    <project>
      <build>
        <plugins>
          ...
          <plugin>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-maven-plugin</artifactId>
            <version>${querydsl.version}</version>
            <executions>
              <execution>
                <goals>
                  <goal>export</goal>
                </goals>
              </execution>
            </executions>
            <configuration>
              <jdbcDriver>org.apache.derby.jdbc.EmbeddedDriver</jdbcDriver>
              <jdbcUrl>jdbc:derby:target/demoDB;create=true</jdbcUrl>
              <packageName>com.mycompany.mydomain</packageName>
              <targetFolder>${project.basedir}/target/generated-sources/java</targetFolder>
            </configuration>
            <dependencies>
              <dependency>
                <groupId>org.apache.derby</groupId>
                <artifactId>derby</artifactId>
                <version>${derby.version}</version>
              </dependency>
            </dependencies>
          </plugin>
          ...
        </plugins>
      </build>
    </project>
    

    二、使用

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

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

    使用QueryDslPredicateExecutor<T>可以简化一些代码,使得查询更加优雅。
    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<String> nameList = queryFactory.select(qm.name).from(qm).fetch();
    //查询实体-selectFrom()
    List<MemberDomain> memberList = queryFactory.selectFrom(qm).fetch();
    //查询并将结果封装至dto中
    List<MemberFavoriteDto> dtoList = queryFactory.select(Projections.constructor(MemberFavoriteDto.class,qm.name,qf.favoriteStoreCode)).from(qm).leftJoin(qm.favoriteInfoDomains,qf).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();
    

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

            //查询条件示例
            List<MemberDomain> 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<MemberDomain> 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<MemberDomain> memberComplexConditionList = queryFactory.selectFrom(qm).where(builder).fetch();
    

    1.2.3 多表查询

    //以左关联为例-left join
    QMemberDomain qm = QMemberDomain.memberDomain;
    QFavoriteInfoDomain qf= QFavoriteInfoDomain.favoriteInfoDomain;
    List<MemberDomain> 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<MemberDomain> subList = queryFactory.selectFrom(qm).where(qm.status.in(JPAExpressions.select(qm.status).from(qm))).fetch();
    

    1.2.6 排序

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

    1.2.7 分页的两种写法

            QMemberDomain qm = QMemberDomain.memberDomain;
            //写法一
            JPAQuery<MemberDomain> query = queryFactory.selectFrom(qm).orderBy(qm.age.asc());
            long total = query.fetchCount();//hfetchCount的时候上面的orderBy不会被执行
            List<MemberDomain> list0= query.offset(2).limit(5).fetch();
            //写法二
            QueryResults<MemberDomain> results = queryFactory.selectFrom(qm).orderBy(qm.age.asc()).offset(2).limit(5).fetchResults();
            List<MemberDomain> 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<MemberDomain> list = queryFactory.selectFrom(qm).where(Expressions.booleanTemplate("{} = \"tofu\"", qm.name)).fetch();
            //上面的写法,当booleanTemplate中需要用到多个占位时
            List<MemberDomain> 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<T>接口。通过注入Repository来使用。

    继承

    @Repository
    public interface IMemberDomainRepository extends JpaRepository<MemberDomain,String>,QueryDslPredicateExecutor<MemberDomain> {
    
    }
    

    注入

    @Autowired
    IMemberDomainRepository memberRepo;
    

    2.1 查询

    简单查询

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

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

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

    QueryDslPredicateExecutor<T>接口提供了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<Integer> order = new OrderSpecifier<>(Order.DESC, qm.age);
    Iterable<MemberDomain> 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<MemberDomain> 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的JPA部分也没什么可写的了。JPA相关的东西都在这里了。把这篇文章吃透querydsl你就很熟悉了。欢迎讨论、勘误~
      • Frank_8b04:请问加 select * from (select * from a) b 这样的查询怎么实现?谢谢~子查询作为外层查询的表
      • XinAnzzZ:大佬技术这么好竟然才写了十几篇博客,别偷懒啊,加油写~~点赞点赞~~
        三汪:我错了哥。这段时间确实忙着偷懒去了哈哈哈哈。技术好是错觉√
      • 规矩先生:请问加 select * from (select * from a) b 这样的查询怎么实现?谢谢~
        规矩先生:@三汪 1.2.5的子查询是在where条件中使用的, 我想让子查询作为外层查询的表. 谢告知~
        三汪:@规矩先生 见1.2.5使用子查询。
      • 章小传:太赞了!大有帮助!
        7efe979c1ddc:同感,,,
      • Nathans:查询条件如果有多个,需要判断该条件是否为空再进行查询,就不能链式操作了?
        三汪:@Nathans 你可能对BooleanBuilder的用法有什么误会。仔细看一下1.2.2应该能懂。
        Nathans:@三汪 举个例子qm.name.eq(val),由于页面是多条件匹配,不是必填,所以类似的val值可能就是空,也没关系?如果为空则where后面不出现该条件匹配?
        三汪:@Nathans 可以。用booleanbuilder
      • Nathans:有个问题请教下,对象绑定的都是对象还是只是一个外键?
        三汪:对不起我没太懂你的意思。解释一下?

      本文标题:SpringBoot环境下QueryDSL-JPA的入门及进阶

      本文链接:https://www.haomeiwen.com/subject/snuowxtx.html