閱讀本文需要Mysql,Maven和SpringBoot基礎(chǔ)知識。
更新日志
- 2018.03.19更新:增加
二、1.2.7 分頁的兩種寫法和二、1.2.8 使用Template實(shí)現(xiàn)QueryDSL未支持的語法 - 2018.01.25更新:增加使用心得(查詢條件中字段為String時關(guān)于null,empty,blank的表達(dá))
- 2018.01.24更新:增加mysql聚合函數(shù)CONCAT,DATE_FORMAT的使用示例
本文由作者三汪首發(fā)于簡書。
Demo已上傳github
一、環(huán)境配置
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(查詢實(shí)體,命名方式為:"Q"+對應(yīng)實(shí)體名)。
上文引入的依賴中querydsl-apt即是為此插件服務(wù)的。
注:在使用過程中,如果遇到query type無法自動生成的情況,用maven更新一下項(xiàng)目即可解決(右鍵項(xiàng)目->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>
補(bǔ)充:
QueryDSL默認(rèn)使用HQL發(fā)出查詢語句。但也支持原生SQL查詢。
若要使用原生SQL查詢,你需要使用下面這個maven插件生成相應(yīng)的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環(huán)境下,我們可以通過兩種風(fēng)格來使用QueryDSL。
一種是使用JPAQueryFactory的原生QueryDSL風(fēng)格,
另一種是基于Spring Data提供的QueryDslPredicateExecutor<T>的Spring-data風(fēng)格。
使用QueryDslPredicateExecutor<T>可以簡化一些代碼,使得查詢更加優(yōu)雅。
而JPAQueryFactory的優(yōu)勢則體現(xiàn)在其功能的強(qiáng)大,支持更復(fù)雜的查詢業(yè)務(wù)。甚至可以用來進(jìn)行更新和刪除操作。
下面分別介紹兩種風(fēng)格的使用方式。
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();
//查詢實(shí)體-selectFrom()
List<MemberDomain> memberList = queryFactory.selectFrom(qm).fetch();
//查詢并將結(jié)果封裝至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();
//獲取首個查詢結(jié)果-fetchFirst()
MemberDomain firstMember = queryFactory.selectFrom(qm).fetchFirst();
//獲取唯一查詢結(jié)果-fetchOne()
//當(dāng)fetchOne()根據(jù)查詢條件從數(shù)據(jù)庫中查詢到多條匹配數(shù)據(jù)時,會拋`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();
如果你覺得上面的寫法不夠優(yōu)雅,我們可以使用QueryDSL提供的BooleanBuilder來進(jìn)行查詢條件管理。
如下
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,更復(fù)雜的查詢關(guān)系也不怕。
例如
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 多表查詢
//以左關(guān)聯(lián)為例-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聚合函數(shù)
//聚合函數(shù)-avg()
Double averageAge = queryFactory.select(qm.age.avg()).from(qm).fetchOne();
//聚合函數(shù)-concat()
String concat = queryFactory.select(qm.name.concat(qm.address)).from(qm).fetchOne();
//聚合函數(shù)-date_format()
String date = queryFactory.select(Expressions.stringTemplate("DATE_FORMAT({0},'%Y-%m-%d')", qm.registerDate)).from(qm).fetchOne();
當(dāng)用到DATE_FORMAT這類QueryDSL似乎沒有提供支持的Mysql函數(shù)時,我們可以手動拼一個String表達(dá)式。這樣就可以無縫使用Mysql中的函數(shù)了。
1.2.5 使用子查詢
下面的用法中子查詢沒有什么實(shí)際意義,只是作為一個寫法示例。
//子查詢
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不會被執(zhí)行
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());
寫法一和二都會發(fā)出兩條sql進(jìn)行查詢,一條查詢count,一條查詢具體數(shù)據(jù)。
寫法二的getTotal()等價于寫法一的fetchCount。
無論是哪種寫法,在查詢count的時候,orderBy、limit、offset這三個都不會被執(zhí)行??梢源竽懯褂?。
1.2.8 使用Template實(shí)現(xiàn)QueryDSL未支持的語法
其實(shí)Template我們在1.2.4 使用Mysql聚合函數(shù)中已經(jīng)使用過了。QueryDSL并沒有對Mysql的所有函數(shù)提供支持,好在它給我們提供了Template特性。我們可以使用Template來實(shí)現(xiàn)各種QueryDSL未直接支持的語法。
示例如下。
QMemberDomain qm = QMemberDomain.memberDomain;
//使用booleanTemplate充當(dāng)where子句或where子句的一部分
List<MemberDomain> list = queryFactory.selectFrom(qm).where(Expressions.booleanTemplate("{} = \"tofu\"", qm.name)).fetch();
//上面的寫法,當(dāng)booleanTemplate中需要用到多個占位時
List<MemberDomain> list1 = queryFactory.selectFrom(qm).where(Expressions.booleanTemplate("{0} = \"tofu\" and {1} = \"Amoy\"", qm.name,qm.address)).fetch();
//使用stringTemplate充當(dāng)查詢語句的某一部分
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好用歸好用,但也有其局限性。
例如當(dāng)我們需要用到復(fù)雜的正則表達(dá)式匹配的時候,就有些捉襟見肘了。這是由于Template中使用了{}來作為占位符,而正則表達(dá)式中也可能使用了{},因而會產(chǎn)生沖突。
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"));
也可以使用更優(yōu)雅的BooleanBuilder 來進(jìn)行條件分支管理
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()會返回滿足查詢條件的數(shù)據(jù)行的數(shù)量,exists()會根據(jù)所要查詢的數(shù)據(jù)是否存在返回一個boolean值,都很簡單,因此不再贅述。
下面著重進(jìn)行介紹findOne()和findAll()兩個關(guān)鍵查詢方法。
2.1.1 findOne()
findOne,顧名思義,從數(shù)據(jù)庫中查出一條數(shù)據(jù)。沒有重載方法。
和JPAQuery的fetchOne()一樣,當(dāng)根據(jù)查詢條件從數(shù)據(jù)庫中查詢到多條匹配數(shù)據(jù)時,會拋NonUniqueResultException。使用的時候需要慎重。
2.1.2 findAll()
findAll是從數(shù)據(jù)庫中查出匹配的所有數(shù)據(jù)。提供了以下幾個重載方法。
- findAll(Predicate predicate)
- findAll(OrderSpecifier<?>... orders)
- findAll(Predicate predicate,OrderSpecifier<?>... orders)
- findAll(Predicate predicate,Sort sort)
第一個重載方法是不帶排序的,第二個重載方法是只帶QueryDSL提供的OrderSpecifier方式實(shí)現(xiàn)排序而不帶查詢條件的,而第三個方法則是既有條件又有排序的。
因此我們直接來看第三個方法的使用示例。
QMemberDomain qm = QMemberDomain.memberDomain;
OrderSpecifier<Integer> order = new OrderSpecifier<>(Order.DESC, qm.age);
Iterable<MemberDomain> iterable = memberRepo.findAll(qm.status.eq("0013"),order);
除了QueryDSL提供的排序?qū)崿F(xiàn),我們還有支持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時關(guān)于null,empty,blank的表達(dá)
(如果你還不了解null,empty,blank的區(qū)別,請先自行搜索了解)
QueryDSL為String類型的字段提供了.isEmpty(),isNull(),.isNotEmpty(),isNotNull()這四個函數(shù)支持,唯獨(dú)沒有對blank提供支持。經(jīng)過測試,我發(fā)現(xiàn)可以通過這種方式來實(shí)現(xiàn)對blank的使用:.eq(""),.ne("")
四、參考
五、擴(kuò)展閱讀
以上。
希望我的文章對你能有所幫助。
我不能保證文中所有說法的百分百正確,
但我能保證它們都是我的理解和感悟以及拒絕直接復(fù)制黏貼(確實(shí)需要引用的部分我會附上源地址)。
有什么意見、見解或疑惑,歡迎留言討論。