【Spring Boot】第7.3課-在 MongoRepository 定義查詢條件與排序方式 #2024 年更新


https://unsplash.com/photos/LmyPLbbUWhA

上一篇已經介紹過 MongoRepository 內建的查詢方法,也就是指定 id 欄位。但資料勢必有其他欄位,只查 id 肯定是不夠的。

本文會使用 Spring Data 框架的功能,示範如何在 repository 中設計自己的查詢條件,包含透過方法名稱及原生語法。接著說明如何進行排序與分頁。

最後簡介 MongoTemplate,藉由動態產生查詢條件,以因應多樣的需求。

本文的練習用專案:
https://github.com/ntub46010/SpringBootTutorial/tree/Ch7-2


一、資料類別

首先讓我們快速回顧上一篇要儲存到資料庫的學生資料類別。

而以下分別是學生的「聯繫方式」與「證照」,屬於學生資料的內部欄位。

並且也建立了叫做「StudentRepository」的介面,繼承 MongoRepository 的基礎方法。


二、自定義查詢方法

我們能在 StudentRepository 中,依照特定的命名規則,設計自己的查詢方法。

本節挑選一部份出來示範。更多命名方式,讀者可參考 Spring Data JPA 官方文件。雖然「JPA」是針對關聯式資料庫,但裡面的內容仍適用於 MongoDB。

(一)相等條件

以下是查詢「name」欄位值等於參數值的方法。

方法名稱中,在 findBy 後面緊接著欄位名稱。該名稱必須存在於文件的資料類別,也就是 Student 類別中,否則 Spring Boot 會啟動失敗。

以下是查詢內部欄位的方法,分別是將聯繫方式與證照當作條件。

想在方法名稱中指定內部欄位,只要將欄位的「路徑」寫出即可。第 1、2 個方法分別是查詢 Contact 物件欄位中的 email 與 phone 欄位。

而第 3 個方法的查詢條件,則是在 Certificates 列表中,只要任一個元素的 type 欄位值為某個值,就算符合。

至於回傳值型態,若讀者認為該條件會有多筆資料符合,可選擇回傳 List。

附帶一提,方法的參數名稱,與資料的欄位名稱並無關聯,所以不一定要取相同的名字。

(二)範圍條件

以下針對數值與日期欄位,做範圍條件的查詢。

第 1、2 個方法,分別是查詢年級大於等於,或小於等於某個值的資料。方法名稱中使用了 GreaterThanEqualLessThanEqual 關鍵字。若範圍不想包含邊界,只要大於或小於,則拿掉 Equal 關鍵字即可。

第 3、4 個方法,分別是查詢生日在某個日期之後或之前的資料。方法名稱中使用了 AfterBefore 做為關鍵字。

(三)組合多個條件

查詢條件也可透過「AND」或「OR」的邏輯組合起來。

以下的條件是 Contact 物件欄位中的 phone 欄位等於某個值,或 email 欄位等於某個值。

方法名稱中使用了 Or 關鍵字,而 AND 邏輯的關鍵字為 And

要注意的是,透過 AND 邏輯組合查詢條件時,欄位不可重複,否則呼叫時會發生例外。舉例來說,「年級大於等於且小於等於」這項條件,不可寫成:

List<Student> findByGradeGreaterThanEqualAndGradeLessThanEqual(int from, int to);

原因是 Spring Data 會將方法名稱解讀成類似下面的條件。

{
    "grade": {
        "gte": 1
    },
    "grade": {
        "lte": 4
    }
}

在 JSON 資料中,若同一階層有相同名稱的欄位,是不合法的。

回到範圍查詢,此時可使用 Between 關鍵字,並傳入 Range 型態的參數。

List<Student> findByGradeBetween(Range<Integer> range);

Range 物件的建立方式為:

Range<Integer> range = Range.of(
    Range.Bound.inclusive(from),
    Range.Bound.inclusive(to)
);

(四)原生語法

讀者也許會覺得上述「findByContactEmailOrContactPhone」的方法名稱太冗長了,可讀性也下降。或者有些比較複雜的條件,很難寫出方法名稱,甚至根本寫不出來。

針對這個情形,Spring Data 也支援我們直接撰寫原生語法,而方法可隨意取名。以下的例子是將相同的查詢條件,以 MongoDB 的語法寫出。

使用 @Query 注解,可以用字串的形式提供語法。而當中的「?0」、「?1」等符號,代表要取用方法的第幾個參數(位置從 0 開始算)。

此外,Spring Data 並不在意語法是否換行與縮排,讀者可自行排版。

以下再提供一個例子,其條件是具有某個證照,且分數大於等於某值,例如多益分數大於等於 900。

由於語法並不是本文的重點,有興趣的讀者,可參考筆者的 MongoDB 系列文章:【MongoDB】如何撰寫查詢語法

三、排序與分頁

(一)排序

以下是根據年級做遞減排序,而沒有查詢條件。

這是一種簡便的做法,直接在方法名稱後方加上 OrderBy 關鍵字,緊接著是欄位名稱與排序方向。

若想要更彈性一點,可選擇在方法傳入 Sort 型態的參數。

在建立 Sort 物件時,能夠設定排序的欄位與方向。為了示範用法,以下在 Controller 準備了 API,透過 query string 接收排序方式。

排序的欄位與方向必須一起提供,或兩者都不提供。若有不合理的值,則呼叫 Sort.unsorted 方法,建立不排序的方式。

呼叫 Sort.Order.ascSort.Order.desc 方法,傳入欄位名稱,分別可建立遞增或遞減的排序方式,以 Order 物件表示。

將一至多個 Order 物件傳入 Sort.by 方法,可得到 Sort 物件。它代表整體的排序規則,實現多重排序也不成問題。

(二)分頁

當資料量太多,實務上會利用「分頁」(pagination)的做法,分批從資料庫取得資料,避免造成資料庫、應用程式或傳輸上的負擔。

分頁通常會與排序一起使用。例如先將資料根據名字欄位做遞增排序,接著每 10 筆資料當作一頁。我們可選擇要取第 1 頁(第 1 ~ 10 筆)、第 2 頁(第 11 ~ 20 筆),依此類推。

要透過 repository 的方法進行排序與分頁,需在方法傳入 Pageable 型態的參數。

這個 Pageable 是一個介面,而 Spring Data 內建了叫做 PageRequest 的實作類別。

呼叫 PageRequest.of 方法,依序傳入「第幾頁」、「每頁筆數」,以及前面提到的 Sort 物件,就能建立出 PageRequest 物件。

分頁的頁數與每頁筆數,同樣也必須一起提供,或兩者都不提供。若有不合理的值,則呼叫 Pageable.unpaged 方法,建立不分頁的方式。

四、因應變化多端的查詢條件

(一)背景

看完這篇文章後,我們在 repository 中設計了許多方法。或許讀者也察覺到了,實務上面對不同的需求,就會有各式各樣的查詢條件,有的還要進行排序或分頁。

舉例來說,假設有一支 API 是 GET /students,用途是查詢多筆資料,那它可能會支援大量的 query string。例如:信箱、手機、年級範圍、生日範圍、證照種類及其分數等。以下為示意程式。

此處將可能收到的 query string 封裝成如下的物件:

當查詢條件複雜一點,前端就會傳遞各種 query string 的組合給 API。若後端總是 case by case,在 repository 設計新方法來因應,並在程式中撰寫要呼叫哪一個 repository 方法的判斷邏輯,那會沒完沒了。

在商業邏輯中,透過 Spring Data 的 repository 進行簡單的查詢是很便利的。然而後端若想兼容各種 query string 的組合,條件寫死的 repository 方法有時並不好用。

(二)MongoTemplate 簡介

根據筆者在前公司的經驗,會使用 MongoTemplate 元件,並搭配動態產生的條件進行複雜查詢,以下是一些用法。

假設查詢條件是「2 年級的學生」,且不做排序與分頁,則示意程式如下:

假設查詢條件是「多益分數達 900 分的 1 年級學生」,則示意程式如下:

從範例中可看出,透過 Criteria 物件,能夠以程式碼動態建立出查詢條件。

筆者在「【Elasticsearch】使用 Java API Client 完成簡易搜尋框架」文章中,有刻過一套動態產生條件的查詢流程。以下提供當時的實作思路,供讀者參考:

  1. 將所有 query string 封裝成一個物件,並透過 Java 的「反射」(reflection)轉換成通用的 Map 資料結構。
  2. 處理排序與分頁的 query string。
  3. 處理需「客製化」的 query string,如範圍條件、內部欄位條件。此步驟每個 collection 應個別處理,需保留擴充的彈性。
  4. 其餘 query string 視為欄位的相等條件。
  5. 將所有條件以 AND 邏輯串接起來。
  6. 執行查詢。

本節只是簡單介紹用法。至於 MongoTemplate 元件的配置方式,以及要如何開發出通用且易於擴充的查詢流程,得自行去打造。

本文的完成專案:
https://github.com/ntub46010/SpringBootTutorial/tree/Ch7-3

上一篇:【Spring Boot】第7.2課-使用 Spring Data 存取 MongoDB 資料庫,進行基本 CRUD 操作

下一篇:【Spring Boot】第8.1課-準備 MySQL 資料庫與認識 Spring Data JPA

留言

  1. 在後面課程使用MongoRepository時, getProducts時沒輸入priceTo的話會自動為0而不是null, 弄得沒法默認為Integer.MAX_VALUE, 該怎麼辦? 是ProductQueryParameter設默認值嗎...

    回覆刪除
    回覆
    1. 嗨,請問是後面的哪個課程呢?我使用第23課的程式做測試後,沒有發現你說的問題。在範例程式中,ProductQueryParameter的priceTo的型態是大寫的「Integer」。所以沒有給值時會是null,不是0。

      刪除
    2. 哦哦, 謝謝提醒, 沒發現自己打了int

      刪除
  2. 當我Post http://localhost:8080/products的時候,Postman出現403.
    後來上網找答案,發現要在class SecurityConfig 裡面,加上
    .antMatchers(HttpMethod.POST, "/products").permitAll()
    這一行,就不會出現403了.

    回覆刪除
    回覆
    1. 這篇文章的完成後專案,沒有加入 Spring Security,照理來說不會有這個問題,反而是在第17課完成才會出現。
      屆時請參考第18課的文章,在 request header 攜帶 access token,就不會出現 403,也不必加上 permitAll 了~

      刪除
  3. 作者已經移除這則留言。

    回覆刪除
  4. 您好 好像沒看到第八課的部分~

    回覆刪除
    回覆
    1. 嗨嗨,原本第 7、8 課都是 MongoDB,但今年做了一些更新,整合成第 7.1 ~ 7.3 課系列。目前第 8 課是「空號」,正在寫 MySQL 的文章 :)

      刪除
    2. 了解~~ 您寫得很好 目前正在學習spring boot相關知識 若有問題再請教您 謝謝~

      刪除

張貼留言