使用 Hibernate 查詢兩個日期之間的記錄
1. 概述
查詢特定時間範圍內的資料是大多數企業應用程式的核心需求。無論是產生每月財務報表還是篩選最近 24 小時的日誌,Hibernate 都提供了多種方法來處理這些時間查詢。
在本教程中,我們將探討如何使用 HQL、Criteria API 和原生 SQL 查詢兩個日期之間的記錄。
2. 設定
為了示範這些操作,我們來定義一個Order實體。雖然舊版的 Hibernate 需要特定的註解來表示日期,但現代版本(Hibernate 5+)可以原生支援 Java 8 的java.time類型:
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String trackingNumber;
private LocalDateTime creationDate;
// Getters and Setters
}
如果我們仍然使用舊版的java.util.Date ,則需要使用@Temporal註解來指定我們想要儲存的是日期、時間還是兩者都儲存:
@Temporal(TemporalType.TIMESTAMP)
private Date legacyCreationDate;
3. 使用 Hibernate 查詢語言 (HQL)
HQL 是 Hibernate 中編寫日期範圍查詢最常用的方法。它可讀性強,可跨資料庫移植,並且允許我們使用實體模型而不是原始 SQL 表。
3.1. 使用BETWEEN關鍵字
BETWEEN運算子是查詢特定範圍內記錄的最直接方法。它包含起始日期和結束日期,這意味著恰好落在起始日期或結束日期上的記錄將包含在結果中:
String hql = "FROM Order o WHERE o.creationDate BETWEEN :startDate AND :endDate";
List<Order> orders = session.createQuery(hql, Order.class)
.setParameter("startDate", startDate)
.setParameter("endDate", endDate)
.getResultList();
雖然這種語法簡潔明了,但在使用LocalDateTime時會引入一個常見的邏輯陷阱。如果我們想要取得 1 月 31 日的所有訂單,但提供的endDate為 2024-01-31 00:00:00,查詢結果幾乎會排除當天所有的訂單。因為該運算符僅包含到午夜截止時間,所以 31 日上午 10:30 或晚上 9:00 下的訂單實際上「大於」endDate 參數,因此會被從結果中排除。
要使用BETWEEN取得完整的最後一天時間,我們需要手動將時間設定為當天的最後一毫秒(23:59:59.999)。為了避免這種不精確的手動計算,更穩健的方法是使用比較運算子來建立一個半開區間。
3.2. 使用比較運算符
當我們查詢日曆邊界(例如整天或整月)時,最安全的模式是使用半開區間:包含下限,不包含上限。這意味著我們使用 >= 表示開始,< 表示結束。
假設我們想要 2024 年 1 月以來的所有訂單。與其嘗試計算 1 月 31 日晚上 11:59:59,我們可以直接使用 2 月 1 日作為我們的最終endDate :
LocalDateTime startDate = LocalDateTime.of(2024, 1, 1, 0, 0, 0);
LocalDateTime endDate = LocalDateTime.of(2024, 2, 1, 0, 0, 0); // exclusive
String hql = "FROM Order o WHERE o.creationDate >= :startDate " +
"AND o.creationDate < :endDate";
List<Order> orders = session.createQuery(hql, Order.class)
.setParameter("startDate", startDate)
.setParameter("endDate", endDate)
.getResultList();
這種模式之所以更受歡迎,是因為它無論資料庫儲存的是微秒、毫秒還是秒,都能正確運作。我們無需擔心會遺失落在邊界上的記錄。
4. 使用 Criteria API
Criteria API 提供了一種以程式設計方式建構日期範圍查詢的方法。這在建立動態搜尋頁面時尤其有用,因為使用者可能需要提供開始日期、結束日期、兩者都提供,或者兩者都不提供。與 HQL 字串不同,Criteria API 是類型安全的,因此我們的 IDE 可以幫助我們在欄位名稱更改時進行重構。
4.1. 使用BETWEEN的基本查詢
對於簡單的包含性查詢,我們可以使用between()方法。就像 HQL 一樣,這會包含恰好落在endDate範圍內的記錄:
CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Order> query = cb.createQuery(Order.class);
Root<Order> root = query.from(Order.class);
query.select(root)
.where(cb.between(root.get("creationDate"), startDate, endDate));
List<Order> orders = session.createQuery(query).getResultList();
4.2. 使用比較運算符
當我們需要一個獨佔的上限時,我們將between()替換為greaterThanOrEqualTo()和lessThan() 。這樣就得到了我們在 HQL 中看到的相同的半開區間模式:
Predicate startPredicate = cb
.greaterThanOrEqualTo(root.get("creationDate"), startDate);
Predicate endPredicate = cb
.lessThan(root.get("creationDate"), endDate);
query.select(root)
.where(cb.and(startPredicate, endPredicate));
4.3 建構動態查詢
當某些篩選條件是可選的時,Criteria API 的優勢就真正體現出來了。我們可以建立一個謂詞物件列表,並僅應用使用者提供的那些篩選條件:
List<Predicate> predicates = new ArrayList<>();
if (startDate != null) {
predicates.add(cb.greaterThanOrEqualTo(root.get("creationDate"), startDate));
}
if (endDate != null) {
predicates.add(cb.lessThan(root.get("creationDate"), endDate));
}
query.select(root).where(predicates.toArray(new Predicate[0]));
這種方法可以保持程式碼的簡潔,避免使用動態 HQL 時會遇到的字串連線問題。
5. 使用原生 SQL
有時我們需要使用 HQL 不支援的資料庫特定日期函數,例如 PostgreSQL 的DATE_TRUNC或 Oracle 的TRUNC 。在這種情況下,Hibernate 允許我們回退到使用原生 SQL 查詢。
以下是如何在 SQL 中直接編寫日期範圍查詢。請注意,我們使用資料庫列名(例如creation_date而不是實體欄位名稱(例如creationDate ):
String sql = "SELECT * FROM orders WHERE creation_date >= :startDate " +
"AND creation_date < :endDate";
List<Order> orders = session.createNativeQuery(sql, Order.class)
.setParameter("startDate", startDate)
.setParameter("endDate", endDate)
.getResultList();
對於更複雜的場景,我們可以利用特定的資料庫功能。例如,如果我們想要完全忽略時間部分,可以使用資料庫特有的函數,例如 PostgreSQL 的DATE_TRUNC或 MySQL 的DATE( ) 函數:
// PostgreSQL
String sql = "SELECT * FROM orders WHERE DATE_TRUNC('day', creation_date) >= :startDate " +
"AND DATE_TRUNC('day', creation_date) < :endDate";
// MySQL
String sql = "SELECT * FROM orders WHERE DATE(creation_date) >= :startDate " +
"AND DATE(creation_date) < :endDate";
雖然原生 SQL 功能強大,但我們應該謹慎使用。我們編寫的每個原生查詢都會將應用程式綁定到特定的資料庫。如果我們將來從 PostgreSQL 切換到 MySQL,就需要重寫這些查詢。一般來說,應該先使用 HQL,只有當 Hibernate 的 HQL 無法表達我們的需求時,才考慮使用原生 SQL。
6. 結論
我們已經介紹了三種在 Hibernate 中查詢兩個日期之間記錄的不同方法。 HQL 因其可讀性和可移植性,在大多數情況下是最佳選擇。 Criteria API 在建立具有可選過濾器的動態查詢時表現出色。原生 SQL 可以作為資料庫特定功能的強大替代方案,但我們應該注意它帶來的廠商鎖定問題。
對於大多數應用程式來說,在 HQL 中使用半開區間模式( >= startDate AND < endDate )是最簡單、最可靠的方法。
與往常一樣,本教學的完整原始碼可 在 GitHub 上找到。