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依赖

        <!-- 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("")

四、参考

五、扩展阅读


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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容