SpringBoot環(huán)境下QueryDSL-JPA的入門及進階

閱讀本文需要Mysql,Maven和SpringBoot基礎(chǔ)知識擅编。


更新日志

  • 2018.03.19更新:增加二、1.2.7 分頁的兩種寫法二燥透、1.2.8 使用Template實現(xiàn)QueryDSL未支持的語法
  • 2018.01.25更新:增加使用心得(查詢條件中字段為String時關(guān)于null,empty,blank的表達)
  • 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(查詢實體班套,命名方式為:"Q"+對應(yīng)實體名)肢藐。
上文引入的依賴中querydsl-apt即是為此插件服務(wù)的。

注:在使用過程中,如果遇到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發(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)在其功能的強大调窍,支持更復(fù)雜的查詢業(yè)務(wù)。甚至可以用來進行更新和刪除操作张遭。

下面分別介紹兩種風(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();
//查詢實體-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來進行查詢條件管理襟雷。
如下

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表達式咧虎。這樣就可以無縫使用Mysql中的函數(shù)了。

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不會被執(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進行查詢,一條查詢count捌显,一條查詢具體數(shù)據(jù)茁彭。
寫法二的getTotal()等價于寫法一的fetchCount
無論是哪種寫法扶歪,在查詢count的時候理肺,orderBy、limit善镰、offset這三個都不會被執(zhí)行妹萨。可以大膽使用炫欺。

1.2.8 使用Template實現(xiàn)QueryDSL未支持的語法

其實Template我們在1.2.4 使用Mysql聚合函數(shù)中已經(jīng)使用過了乎完。QueryDSL并沒有對Mysql的所有函數(shù)提供支持,好在它給我們提供了Template特性品洛。我們可以使用Template來實現(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ù)雜的正則表達式匹配的時候岛宦,就有些捉襟見肘了台丛。這是由于Template中使用了{}來作為占位符,而正則表達式中也可能使用了{},因而會產(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 來進行條件分支管理

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值蚁趁,都很簡單,因此不再贅述实胸。
下面著重進行介紹findOne()findAll()兩個關(guān)鍵查詢方法他嫡。

2.1.1 findOne()

findOne,顧名思義庐完,從數(shù)據(jù)庫中查出一條數(shù)據(jù)钢属。沒有重載方法。
JPAQueryfetchOne()一樣门躯,當(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方式實現(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的表達

(如果你還不了解null,empty,blank的區(qū)別锅铅,請先自行搜索了解)
QueryDSL為String類型的字段提供了.isEmpty(),isNull(),.isNotEmpty(),isNotNull()這四個函數(shù)支持,唯獨沒有對blank提供支持。經(jīng)過測試减宣,我發(fā)現(xiàn)可以通過這種方式來實現(xiàn)對blank的使用:.eq(""),.ne("")

四盐须、參考

五、擴展閱讀


以上贼邓。
希望我的文章對你能有所幫助。
我不能保證文中所有說法的百分百正確闷尿,
但我能保證它們都是我的理解和感悟以及拒絕直接復(fù)制黏貼(確實需要引用的部分我會附上源地址)塑径。
有什么意見、見解或疑惑填具,歡迎留言討論统舀。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末匆骗,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子誉简,更是在濱河造成了極大的恐慌碉就,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件闷串,死亡現(xiàn)場離奇詭異瓮钥,居然都是意外死亡,警方通過查閱死者的電腦和手機烹吵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門碉熄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人肋拔,你說我怎么就攤上這事具被。” “怎么了只损?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長七咧。 經(jīng)常有香客問我跃惫,道長,這世上最難降的妖魔是什么艾栋? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任爆存,我火速辦了婚禮,結(jié)果婚禮上蝗砾,老公的妹妹穿的比我還像新娘先较。我一直安慰自己,他們只是感情好悼粮,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布闲勺。 她就那樣靜靜地躺著,像睡著了一般扣猫。 火紅的嫁衣襯著肌膚如雪菜循。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天申尤,我揣著相機與錄音癌幕,去河邊找鬼。 笑死昧穿,一個胖子當(dāng)著我的面吹牛勺远,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播时鸵,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼胶逢,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起宪塔,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤磁奖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后某筐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體比搭,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年南誊,在試婚紗的時候發(fā)現(xiàn)自己被綠了身诺。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡抄囚,死狀恐怖霉赡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情幔托,我是刑警寧澤穴亏,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站重挑,受9級特大地震影響嗓化,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜谬哀,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一刺覆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧史煎,春花似錦谦屑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至恬偷,卻和暖如春充蓝,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背喉磁。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工谓苟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人协怒。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓涝焙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親孕暇。 傳聞我的和親對象是個殘疾皇子仑撞,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容