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

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


更新日志

  • 2018.03.19更新:增加二、1.2.7 分页的两种写法二、1.2.8 使用Template实现QueryDSL未支持E j 7 h t ] Y的语法
  • 2018.01.25更新:增加使用心得(& F Q # X查询条件中字段为String时关于null,s 9 = : 2 sempty,blank的表达)
  • 2018.01@ g p 2.24更新:` K ) P $ W增加mysql聚合函数CONCAT,DATE_FORMAT的使用示例

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

一、环境配置

1. 引入mavenm j c / R u依赖

        <!--{ o ` querydsl -Y 4  _ &->
<dependency&N 1 t k i T : Igt;
<groupId>1 A ; U;com.querydsl</groupId>
<? - 2 H 6 Y . };ar[ { xtifactId>querydsl-jpa</artifactId>
</dependency>
<dependency>
<groupId& l q 9 ? - ^gt;com.querydsl</groupId>
<artifactId>querydsl-apt</ar= K 2 0 ; P /tifactId>
<scope>provided&g 1 [ P @lt;/scope>
</depd u hende! 3 i Rncy>

2. 添加maven插件

添加这个插件是为了让程序自动生成query type(查询实体,命名方式为:"Q"+对应实体名)。
上文引入的依赖中queryd% P _ 7 Lsl-a1 | { T T s 3 F Ypt~ : 0 * U | m即是为此插G s J O件服务的。

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

            <plugin>
<groupId>com.mysema.maven</groupV X C K k F e e XId>
<artifactId>apt-maven-plugin</artifact^ X Y ) 3 v }Id>
<version>1.1.3&l0 S = r Z * 2t;/versiW ` Son>
<executions>
<execution>
<goals>
<goal>process</goaJ | xl>
</goa Y # y &ls>
<configuration>
<outputDi. g l 6 n Wrectory>target{ f h H W m X R/generated-sources/j_ 0 h -ava</outp[ | 6 : l 6 ( _utDire6 5 w - 3 ! ~ctory>
<processor>com[ i 1 , W.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration: ; K f + Z m Z>
</execution>
</executions>
</plugin7 B f B v>

补充:
QueryDSL默认使用HQL发出查询语句。但也支持原生SQL查询n 7 l G - F - &。
若要使用原生SQL查] 8 7 q O E y询,你需要使用下面这个maven插件生成相应的query type。

<project>
<build>
<E $ ; x / # * e;plugiU A G 5 S f 3ns>
...
<plugin>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-maven-plugin</artifactId>
<version>${querydsl.version}</v* m x [ersion>
<executions>
<exeW c &  p Tcution>
<goals>
<goal&1 Z 1gt;export</goalD 9 e J R B>
</goals>
</execution>
<2 G ];/executions>
<configuration&gd Y L { F q 1t;
<jdbcDriver>org? P W 0 ? ^.apache.derby.jdbc.EmbeddedDriver</jdbcDriver>
<jdbcUrl>jdbc:derby:target/demo{ ^ I } G F P 0 ~DB;create=true</jdbcUrl>
<packageName&gy y Pt;com.mycompany.mydomain</packageName>
<targetFolder>${project.basedir}/- 6 ! E : k !target/generate1 / DdA % r U v V @ W-sN K n X L J Y 5 Xources/javam f 7 X   * Y</targetFolder>
</configuration>
&{ B ] ]lt;dependencies>
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derby</artifactId>
<version>${derby.version}</version>
</dependencyI U n f>
</dependencies>
</plugin>
...
</plugins>
<4 E 5 Q X J q 7;/build/ + Q C j t F>
<q e | y/project>

二、使用

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

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

使用Qu0 Z W 0 B u { ^ _eryDslPredicateExecutor<T>可以简化一些代码,l % @使得查询更加优雅。
JPAQueryFactory的优势则体现在其功能的强大,支持更复杂的查询业务。甚至可以用来进行更新和删除操作。

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

1. JPAt [ dQueryFactory

JPAQueryFactory使用逻E { ( | ` O X g辑类似于HQL/SQL语法,不再额外 k c 7 _ : v说明。
Quero 1 ) 7 ~ E 1 MyDSL在支持JPA的同时,也提供了对Hid i T / g ubernate的支持。可以通过HibernateQueryFactory来使5 ~ L X s %用。

装配

    @Bean
@Autowired
public JPAQuer2 , ? iyFactory jpaQuew i 6 . F { + 2 -ry(EntityManage^ % ^ dr entityMd K ! ` f /anager) {
reE - R m { kturn new JPAQueryFactory(entityManager);
}

注入

    @Autowired
JPAQueryFactory queryFactory;

1.1 更新/删除

Update

QMemberDomain q` l H 7 N Sm = QMem/ d T QberDomain.memberDoma[ x 9 #in;
queryFactory.update(qm).X y x h ? T 2 0set(qm.status, "0012").where(qm.status.eM C S X oq("0011")).execute();

Dj T ? a =elete

QMemberDomain q$ 1 | W ^ z fm = QMemberDomain.memberDomain;
queryFactory.delete(qm).where(qm.status.eq("0012")).execute();

1.2 查询

查询^ / E /简直可以玩出花来。

1.2.1 selec G T S ?ct()和fetm r n M = ch()的几种常用写法

QMemberDomain qm = QMemberDomain.memberDomh S f Tain;
//查询字段-se$ $ Z dlect()
List<String> nameList = queryFactory.select(qm.name).from(qm).fetch();
//查询实体-selectFrom()
List<MemberDomain> memberList = queryFactory.selectFrom(qm).fetch();
//查询并将结果封装V 4 S P = 5 ~ e至dto中
List<MemberFavoriteDto> dtoList = queryFact! O c t Qory.select(Projections.constructor(MemberFavoriteDto.class,qm.name,qf.favoriteStoreCode)).from(qm).leftJoin(qm.favorite; D * # InfoDomains,qf).fetchm h h();
//去重查询-selectDistinct()
List<String> distinctNameList = queryFF 7 | ( I J $ vactory.selectDistinct(qm.name).from(qm).fetc| . { c ] I dh();
//获取首个查询结果-fetchFirst()
MemberDomain firstMember = queryFactory.selectFrom(qm).fetchFirst();
//获取唯一查询结果-fetchOne()
//当fetchOne()根据查询条件从数据库中查询到多条匹配数据时,会抛`NonUniqueResultException`。
MembeQ H * J a 9 z (rDomain anotherFirstMember = queryFactory.selectFrom(qm).fetchOne();

1.2d Q o b - 7 F i.# e h h p 2 -2 where子句查询条件的几种常用写法

        //查询条件示例
List<MemberDom: ` b  { 9 G + Dain> memberConditionList = queryFactory.seY f = V A m PlecJ U e 2 x s l %tFrom(qm)
//like示例
.where(qm.name.like('S / R m q | F |%'+"Jack"+'%')
//contain示例[ l f B C ( Q d ?
.and(qm.address.contains("厦门"))
//equal示例
.and(qm.status.eq("0013"))
//between
.and(qm.age.Z ^ o ( tbetween(20, 30)))
.fetch();

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

BooleanBuilder builder = new Bo5 { u @ P goleanBuilder();
//like
builder.and(qm.name.like('%'+"Jack"+'%'));
//contain
builder.and(qm.address.contay e I X l h l ] ^ins("厦门"));
//equal示例
builder.and(qm.status.eq("0013"));
//between
builder.andd J (  %(qm.age.b 7 V % kbetween(20, 30));
List<MemberDomain> memberConditionList =e 4 , queryFactory.selectFrom(qm).where(builder).fetch();

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

Bo4 # ) W F @ 5 YoleanBuilder builder = new BooleanBuilder();
builder.and(qm.q j k K /address.contaj . ! O 3 7 R ( ]ins("厦门"));
BooleanBuilder builder2 = new BooleanBuilder();
builder2.or(qm.status.eq(O 1 E G"0013"));
builder2.or(qm.status.eq("0014"));
builder.and(builder2);
List<MemberDomain> memberComplexConditionList = queryFactory.selectFrom(qm).where(builder).fetch();

1.2.3 多表查询

//5 y 3 _以左关联为例-left join
QMemberDomain qm = QMemberDomain.memberDomain;
QFavoriteInfoDomain qf= QFavoriteInx o G A j Y WfoDom] B . o S 9 ?ain.favoriteInfoDomain;
List<MemberDomain> leftJoinList = queryFacto - } r U O 0 eory.s~ - V % } | u zeleI ~ 9 7 c R f vctFrom(qm).leftJoin(qm.favoriteInfoDomains,qf).where(q~ s / t j ?f.favoriteStoreCodM A ee.eq("0721")).fetch0 P +();

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))V H h 5 J a [ J .from(qm)Z 5 b ` L % 1 N (.fetchOne();

当用到DATE_FORMAT这类QueryDSL似乎没有提供支持的Mysqh $ E + 7 8 $ -l函数时,我们可以手动拼一个V A o & EString表达式。这样就可以无缝使用Mysql中的函数了。

1.2.5 使用子查询

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

//子查询
List<MemberDomain&i M 0 0 j o F 3 .gt; subList = queryFactory.selectFrom(qm).where(qm.status.inJ / . P ((JPAExpressions.select(qm.status).from(qm))).fetch();

1.2.6 排序

//排} T ] j序
List<MembeY C } ; d P w ` WrDomain> orderList = queryFactory.selectFrom(qm).orderBy(qm.name.asc()).% # Pfetch(] O 7 o ! [ x);

1.2.7 分页的两种写法

        QMemberDomaink G F & 0 Q i A qm = QMeW P Y ~ 5 Nmbe- } k q % 3 [rDomain.memberDomain;
//写法一
JPAQuery<MemberDomain> query = queryFactory.selectFrom(qm).orderBy(qm.age.asc());
long total = query.fetc1 # V n o C ` U xhCount();//hfetchCount的时候上面的orderBy不会被执行
List<MemberDomain> list0= query.offset(2).liK n ! pmit(5b g /).fetch();
//写法二
QueryResults<MemberDomain> results =b ! N Q ! g queryFactory.selectFrom(qm).orderByD H ( i - d(qm.age.asG 9 & 2 K 3c()).offset(2).limit(5).? r q p G 5fetchResults();
List<MemberDomain> list = results.getResults();
logger.debug("total:"n V % Z ? h+resultsm * o t b  7.getTotal());
logger.debug("limit:C ~ - p ^ 7"+results.getLimit());
logger.debug("offse[ & } 2 et:"+results.getOffset());

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

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

其实Template我们在1.2.4 使用Mysql聚合S U k函数中已经使用过了。QueryDSL并没有对Mysql的所有函数提供支持,好在它给我们提供了Template特性。我们可以使用Templat. # O ; Me来实现各种QueryDSL未直接支持的语T { B法。
示例如下。B $ @ G A G

        QMem? ~ nberDomaX { w A _ * H Fin qm = QMemberDomain.memberDomain;
//使用booleanTemplate充当where子句或where子句的一部分
List<MemberDomain> list = queryFactory.sele8 + D ! ~ ?ctFrom(qm).where(Expressions.booleanTemplaB O . z w [   ate("{} = \"tofu\"", qm.name))^ | ( } m a l.fetch();
//上面的写法,当booleanTemplate中需要用到多个占位时
List<MemberDomain> list1 = queryFactory.selectFrom(qm).where(Expressions.booleanTemplate("{0} = \"tofu\" and {1} = \] @ q Z c | b * ?"Amoy\"", qm.name,qm.address)).fetch();
//使用stringTempla-  a N @ 4 _ !te充当查询语句的某一部W I ! h分
String date = queryFactory.select(Expressions.stringTemplate("DATE_FORMAT({0},'%Y-%m-%d')", qm.registerDate)).fz t n Y A a s N ~rom(qm).fetchFirst();
//在where子句中使用stringTemplate
String id = queryFactory.select(qm.id).from(qm).where(Expressions.stringTemplate_ 9 P("DAi : ) B q T F U MTE_FORMAT({0},'%Y-%m-%d')", qm.registerDate).eq("2018-03-19")).fetc6 = S $ 7 g  , /hFirst();

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

2. QueryDslPredicateExecutor

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

继承

@RepositW R c m ^ory
public interfaH ` W @ _ce IMemberDomainRepository exts 1 ^ z Q Kends JpaRepository<MemberDoU 0 6 n E w amain,String>,QueryDsle $ 0 L : $ 5PredicateExecutor<n X % G U ( 8 C;Membe3 t K )rDomain>) B 4 N; {
}

@ Q / Q # 9 #

@Autowi9 m gred
IMemberDomainRepositorf T v / , U Qy memberRepo;

2.1 查询

简单查询

QMemberDomain qm = QMemberDomain.membery 7 a O UDomain;
Iterable<MemberDomain> iterable = memberRepo.findAll(qm.st# ~ Matus.eq("0013"));

也可以使用更优雅的BooleanBuilder 来进行条件分支管理9 h x f 5 E g ] g

BooleanBuilder builder = new B4 L Q t I ^ # xooleanBuilder();
builder.and(qm.address.contains("厦门"));
builder.and(qm.status.eq_ [ 1 & j ^ 4("0013"));
Iterable<Memberx = c d S k QDomain> iterable2 = memberRepo.fc T ZindAll(builder);

QueryD9 t % : } E mslPredicateExec8 1 8 d Z ~utor<T>接口提供了findOne(Z 7 I $ 1 k r % l),findAll(),count(),exists()四个方法来支持查询。
count()会返回满足查询条件的数据行的数量,exists()会根据A N w X ( G d 3 }所要查询的数据是否存在返回一个boolean值,都很简单,因此不再赘述。
下面着重进行介绍findOT $ y u Mne()findAll()两个关键查询方{ 4 9法。

2.1.1 findOne()

findOne,顾名思义,从数据库中查出y f r Y : i Q ~一条数据。没有重载方法。
; 9 ? & c uJPAQueryw k N C { 8fetchOne()一样,当根据查询条V W `件从数据库中查询到多条匹配数据时,会抛NonUniqueResultException。使用的时候s V K需要慎重。

2.1.2 findAll()

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

  • findAll(PreL 4 idicate predicate)
  • findAll(OrderSp[ F H 1 9 / ,ecifier<?>... orderV $ C b W & x Is)
  • findAll(Predicate predicate,Order( k ? V s F LSpecifier<?>... orders)
  • findAll(Predicate predicate,Sort sort)

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

QMemberDomain qm = QMemberDomain.mem5 ! l w f G |berDomain;
OrderSpecifier<Integer> order = new OrderSpecifier&lK / ft;&s i P % a ` K % jgt;! l p V w } ! Y(Order.DESC, qm.age);
Iterable<MemberDomU 4 @ Jain> iterable = memberRepo.findAll(qm.status.eq("0013"),o2 @ l l  . I 8rder);

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

QMember7 1 A wDomainT K _ ~ t Y qm = QM : ]emberDomain.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,emptU o . L : H (y,blank的区别,请先自行搜索了解)
QueryDSL为String类型的字段提供了.isEmpty(),isNull(),.isNotEmpty(),isNotNull()这四个函数支持,唯独没有对blank提供支M a ` y w e L持。经过测试,我发现可以通过这种方式来实现对blank的使用:.eq(""),.ne("")

四、参考

  • Querydsl Reference Guide
  • QueryDSLK K 1 J S通用查询框架学习目录
  • querydsl7 1 I c - T ( E e查询使用函数DATE_FA X D ! H 3 - ] &ORMAT

五、扩展阅读

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

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