1. 程式人生 > >Spring Boot (六): 為 JPA 插上翅膀的 QueryDSL

Spring Boot (六): 為 JPA 插上翅膀的 QueryDSL

在前面的文章中,我們介紹了 JPA 的基礎使用方式,《Spring Boot (三): ORM 框架 JPA 與連線池 Hikari》,本篇文章,我們由入門至進階的介紹一下為 JPA 插上翅膀的 QueryDSL。

1. 引言

不可否認的是 JPA 使用是非常方便的,極簡化的配置,只需要使用註解,無需任何 xml 的配置檔案,語義簡單易懂,但是,以上的一切都建立在單表查詢的前提下的,我們可以使用 JPA 預設提供的方法,簡單加輕鬆的完成 CRUD 操作。

但是如果涉及到多表動態查詢, JPA 的功能就顯得有些捉襟見肘了,雖然我們可以使用註解 @Query ,在這個註解中寫 SQL 或者 HQL 都是在拼接字串,並且拼接後的字串可讀性非常的差,當然 JPA 還為我們提供了 Specification

來做這件事情,從我個人使用體驗上來講,可讀性雖然還不錯,但是在初學者上手的時候, PredicateCriteriaBuilder 使用方式估計能勸退不少人,而且如果直接執行 SQL 連表查詢,獲得是一個 Object[] ,型別是什麼?欄位名是什麼?這些都無法直觀的獲得,還需我們手動將 Object[] 對映到我們需要的 Model 類裡面去,這種使用體驗無疑是極其糟糕的。

這一切都在 QueryDSL 出世以後終結了, QueryDSL 語法與 SQL 非常相似,程式碼可讀性非常強,異常簡介優美,,並且與 JPA 高度整合,無需多餘的配置,從筆者個人使用體驗上來講是非常棒的。可以這麼說,只要會寫 SQL ,基本上只需要看一下示例程式碼完全可以達到入門的級別。

2. QueryDSL 簡介

QueryDSL 是一個非常活躍的開源專案,目前在 Github 上的釋出的 Release 版本已經多達 251 個版本,目前最新版是 4.2.1 ,並且由 Querydsl Google組 和 StackOverflow 兩個團隊提供支援。

QueryDSL 是一個框架,可用於構造靜態型別的類似SQL的查詢。可以通過諸如 QueryDSL 之類的 API 構造查詢,而不是將查詢編寫為內聯字串或將其外部化為XML檔案。

例如,與簡單字串相比,使用 API 的好處是

  • IDE中的程式碼完成

  • 幾乎沒有語法無效的查詢

  • 可以安全地引用域型別和屬性

  • 更好地重構域型別的更改

3. QueryDSL 使用實戰

3.1 引入 Maven 依賴

程式碼清單:spring-boot-jpa-querydsl/pom.xml
***

<!--QueryDSL支援-->
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
    <scope>provided</scope>
</dependency>
<!--QueryDSL支援-->
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
</dependency>
  • 這裡無需指定版本號,已在 spring-boot-dependencies 工程中定義。

3.2 新增 Maven 外掛

新增這個外掛是為了讓程式自動生成 query type (查詢實體,命名方式為:"Q"+對應實體名)。
上文引入的依賴中 querydsl-apt 即是為此外掛服務的。

注:在使用過程中,如果遇到 query type 無法自動生成的情況,用maven更新一下專案即可解決(右鍵專案 -> Maven -> Update Folders)。

程式碼清單:spring-boot-jpa-querydsl/pom.xml
***

<plugins>
    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    <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>
</plugins>

3.3 更新和刪除

在 JPA 中已經為我們提供了非常簡便的更新和刪除的使用方式,我們完全沒有必要使用 QueryDSL 的更新和刪除,不過這裡還是給出用法,供大家參考:

程式碼清單:spring-boot-jpa-querydsl/src/main/java/com/springboot/springbootjpaquerydsl/service/impl/UserServiceImpl.java
***

@Override
public Long update(String id, String nickName) {
    QUserModel userModel = QUserModel.userModel;
    // 更新
    return queryFactory.update(userModel).set(userModel.nickName, nickName).where(userModel.id.eq(id)).execute();
}

@Override
public Long delete(String id) {
    QUserModel userModel = QUserModel.userModel;
    // 刪除
    return queryFactory.delete(userModel).where(userModel.id.eq(id)).execute();
}

3.2 查詢

QueryDSL 在查詢這方面可以說玩的非常花了,比如一些有關 select()fetch() 常用的寫法如下:

程式碼清單:spring-boot-jpa-querydsl/src/main/java/com/springboot/springbootjpaquerydsl/service/impl/UserServiceImpl.java
***

@Override
public List<String> selectAllNameList() {
    QUserModel userModel = QUserModel.userModel;
    // 查詢欄位
    return queryFactory.select(userModel.nickName).from(userModel).fetch();
}

@Override
public List<UserModel> selectAllUserModelList() {
    QUserModel userModel = QUserModel.userModel;
    // 查詢實體
    return queryFactory.selectFrom(userModel).fetch();
}

@Override
public List<UserDTO> selectAllUserDTOList() {
    QUserModel userModel = QUserModel.userModel;
    QLessonModel lessonModel = QLessonModel.lessonModel;
    // 連表查詢實體並將結果封裝至DTO
    return queryFactory
            .select(
                    Projections.bean(UserDTO.class, userModel.nickName, userModel.age, lessonModel.startDate, lessonModel.address, lessonModel.name)
            )
            .from(userModel)
            .leftJoin(lessonModel)
            .on(userModel.id.eq(lessonModel.userId))
            .fetch();
}

@Override
public List<String> selectDistinctNameList() {
    QUserModel userModel = QUserModel.userModel;
    // 去重查詢
    return queryFactory.selectDistinct(userModel.nickName).from(userModel).fetch();
}

@Override
public UserModel selectFirstUser() {
    QUserModel userModel = QUserModel.userModel;
    // 查詢首個實體
    return queryFactory.selectFrom(userModel).fetchFirst();
}

@Override
public UserModel selectUser(String id) {
    QUserModel userModel = QUserModel.userModel;
    // 查詢單個實體,如果結果有多個,會拋`NonUniqueResultException`。
    return queryFactory.selectFrom(userModel).fetchOne();
}

3.4 複雜查詢操作

上面列舉了簡單的查詢,但實際我們會遇到相當複雜的操作,比如子查詢,多條件查詢,多表連查,使用示例如下:

程式碼清單:spring-boot-jpa-querydsl/src/main/java/com/springboot/springbootjpaquerydsl/service/impl/LessonServiceImpl.java
***

@Service
public class LessonServiceImpl implements LessonService {

    @Autowired
    JPAQueryFactory queryFactory;

    @Override
    public List<LessonModel> findLessonList(String name, Date startDate, String address, String userId) throws ParseException {
        QLessonModel lessonModel = QLessonModel.lessonModel;
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        // 多條件查詢示例
        return queryFactory.selectFrom(lessonModel)
                .where(
                        lessonModel.name.like("%" + name + "%")
                        .and(lessonModel.address.contains(address))
                        .and(lessonModel.userId.eq(userId))
                        .and(lessonModel.startDate.between(simpleDateFormat.parse("2018-12-31 00:00:00"), new Date()))
                )
                .fetch();
    }

    @Override
    public List<LessonModel> findLessonDynaList(String name, Date startDate, String address, String userId) throws ParseException {
        QLessonModel lessonModel = QLessonModel.lessonModel;
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

        // 動態查詢示例
        BooleanBuilder builder = new BooleanBuilder();

        if (!StringUtils.isEmpty(name)){
            builder.and(lessonModel.name.like("%" + name + "%"));
        }

        if (startDate != null) {
            builder.and(lessonModel.startDate.between(simpleDateFormat.parse("2018-12-31 00:00:00"), new Date()));
        }

        if (!StringUtils.isEmpty(address)) {
            builder.and(lessonModel.address.contains(address));
        }

        if (!StringUtils.isEmpty(userId)) {
            builder.and(lessonModel.userId.eq(userId));
        }

        return queryFactory.selectFrom(lessonModel).where(builder).fetch();
    }

    @Override
    public List<LessonModel> findLessonSubqueryList(String name, String address) {
        QLessonModel lessonModel = QLessonModel.lessonModel;
        // 子查詢示例,並無實際意義
        return queryFactory.selectFrom(lessonModel)
                .where(lessonModel.name.in(
                        JPAExpressions
                                .select(lessonModel.name)
                                .from(lessonModel)
                                .where(lessonModel.address.eq(address))
                ))
                .fetch();
    }
}

3.5 Mysql 聚合函式

QueryDSL 已經內建了一些常用的 Mysql 的聚合函式,如果遇到 QueryDSL 沒有提供的聚合函式也無需慌張, QueryDSL 為我們提供了 Expressions 這個類,我們可以使用這個類手動拼接一個就好,如下示例:

程式碼清單:spring-boot-jpa-querydsl/src/main/java/com/springboot/springbootjpaquerydsl/service/impl/UserServiceImpl.java
***

@Override
public String mysqlFuncDemo(String id, String nickName, int age) {

    QUserModel userModel = QUserModel.userModel;

    // Mysql 聚合函式示例

    // 聚合函式-avg()
    Double averageAge = queryFactory.select(userModel.age.avg()).from(userModel).fetchOne();

    // 聚合函式-sum()
    Integer sumAge = queryFactory.select(userModel.age.sum()).from(userModel).fetchOne();

    // 聚合函式-concat()
    String concat = queryFactory.select(userModel.nickName.concat(nickName)).from(userModel).fetchOne();

    // 聚合函式-contains()
    Boolean contains = queryFactory.select(userModel.nickName.contains(nickName)).from(userModel).where(userModel.id.eq(id)).fetchOne();

    // 聚合函式-DATE_FORMAT()
    String date = queryFactory.select(Expressions.stringTemplate("DATE_FORMAT({0},'%Y-%m-%d')", userModel.createDate)).from(userModel).fetchOne();

    return null;
}

4. 小結

有關 QueryDSL 的介紹到這裡就結束了,不知道各位讀者看了上面的示例,有沒有一種直接讀 SQL 的感覺,而且這種 SQL 還是使用 OOM 的思想,將原本 Hibernate 沒有做好的事情給出了一個相當完美的解決方案,上手簡單易操作,而又無需寫 SQL ,實際上我們操作的還是物件類。

5. 示例程式碼

示例程式碼-Github

示例程式碼-Gitee

6. 參考

《QueryDSL 官方文件