如何使用 LocalDate 值查詢 JPA LocalDateTime 字段
1. 簡介
在 JPA 中使用資料庫時,通常會將時間戳記儲存在資料庫表中,例如createdAt或updatedAt 。但有時我們只有一個LocalDate ,例如當使用者從日曆中選擇一個日期。在這種情況下,我們可能希望找到所有屬於該日期的記錄,無論它們的特定時間。
如果我們嘗試直接將LocalDateTime欄位與LocalDate值進行比較,查詢將不會符合任何內容,因為這兩個資料類型不同。在本教程中,我們將探索在 JPA 中使用LocalDate值查詢LocalDateTime欄位的幾種方法。
2. 理解核心問題
假設我們有一個具有LocalDateTime類型欄位的實體:
@Entity
public class MyClass {
@Id
private UUID id;
private LocalDateTime createdAt;
// Getters and setters
}
儲存庫定義如下:
@Service
public interface IRepository extends CrudRepository<MyClass, UUID> {
void deleteByCreatedAt(LocalDate createdAt);
}
乍一看,這似乎是有效的,因為方法名稱遵循 Spring Data 約定,而createdAt似乎與欄位名稱相符。但是,當我們使用普通的LocalDate呼叫它時:
repository.deleteByCreatedAt(LocalDate.of(2024, 01, 15));
JPA 拋出例外:
org.springframework.dao.InvalidDataAccessApiUsageException:
Parameter value [2024-01-15] did not match expected type [java.time.LocalDateTime (n/a)];
發生此錯誤的原因是,資料庫中的createdAt列的類型為LocalDateTime ,但儲存庫方法接收的是LocalDate 。 Spring Data JPA 不會自動在兩者之間進行轉換,因為一個包含時間元件,而另一個不包含。
LocalDateTime包含日期和時間資訊(例如, 2024-01-15T14:30:45 ),而LocalDate僅包含日期資訊(例如, 2024-01-15 )。
因此,當我們執行如下查詢時:
WHERE created_at = '2024-01-15'
資料庫會尋找與該值exactly相符的時間戳記(包括時間)。由於實際時間戳通常包含小時、分鐘和秒,因此此相等比較找不到結果。
為了正確找到當天的所有記錄,我們需要查找所有位於整個日期範圍內的createdAt值——從午夜( 2024-01-15T00:00:00 )到下一個午夜( 2024-01-16T00:00:00 )但不包括該時間。這需要我們將LocalDate轉換為LocalDateTime範圍。
3. 項目設定
要在 Spring Boot 專案中使用 JPA,我們需要包含[spring-boot-starter-data-jpa](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-jpa)依賴項以及資料庫驅動程式。在本教程中,我們將使用H2來說明範例。
將以下依賴項新增至我們的pom.xml檔:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
3.1. 定義實體
接下來,我們將定義一個簡單的實體來表示我們的資料庫表。
我們稱之為Event 。它將具有ID 、 name和LocalDateTime類型的createdAt欄位:
@Entity
@Table(name = "events")
public class Event {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public Event() {}
public Event(String name, LocalDateTime createdAt) {
this.name = name;
this.createdAt = createdAt;
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Getters and setters
}
該實體直接對應到資料庫中名為events表。 @Entity註解告訴 JPA 此類別應視為託管實體,而@Table(name = “events”)註解則指定要使用的確切表名。
createdAt欄位使用LocalDateTime類型,該類型儲存建立日期和時間,例如2025-10-12T14:35:20這樣的值。 @PreUpdate註解會自動處理時間戳記填滿。
3.2. 準備樣本數據
為了使範例和測試案例更容易理解,我們還需要在 H2 資料檔案中準備一些範例資料。我們可以使用src/main/resources下的data.sql檔案-Spring Boot 會在啟動時自動執行它。
建立一個名為data.sql的檔案並新增以下內容:
INSERT INTO events (name, created_at, updated_at) VALUES
('Morning Meeting', '2025-10-12T09:00:00', '2025-10-12T09:00:00'),
('Lunch Discussion', '2025-10-12T12:30:00', '2025-10-12T12:30:00'),
('Evening Review', '2025-10-12T18:45:00', '2025-10-12T18:45:00'),
('Next Day Planning', '2025-10-13T10:00:00', '2025-10-13T10:00:00');
為了確保它在 Hibernate 建立events表後仍然運行,我們必須將此行新增到我們的 application.properties 中:
spring.jpa.defer-datasource-initialization=true
4. 按日期範圍查詢
使用LocalDate值查詢LocalDateTime欄位最可靠的方法是執行範圍查詢。我們不再比較兩種類型是否相等,而是將LocalDate轉換為代表一整天的起始和結束邊界。
這意味著我們將取得createdAt大於或等於一天的開始時間且小於第二天的開始時間的所有記錄。
我們首先在 JPA 儲存庫中定義一個查詢方法:
public interface EventRepository extends JpaRepository<Event, Long> {
List<Event> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
}
此方法會自動產生一個查詢來取得createdAt介於兩個給定時間戳記之間的所有記錄。
現在,我們可以透過將LocalDate轉換為一系列LocalDateTime值來輕鬆查詢特定日期的事件:
LocalDate date = LocalDate.of(2025, 10, 12);
LocalDateTime startOfDay = date.atStartOfDay();
LocalDateTime endOfDay = date.plusDays(1).atStartOfDay();
List results = eventRepository.findByCreatedAtBetween(startOfDay, endOfDay);
assertEquals(3, results.size());
在此範例中, startOfDay表示午夜( 2025-10-12T00:00:00 ), endOfDay表示第二天的午夜( 2025-10-13T00:00:00 )。
透過使用這兩個邊界值,查詢將捕獲 10 月 12 日創建的所有事件,無論具體時間是什麼。
在底層,Spring Data JPA 產生類似這樣的 SQL 查詢:
SELECT * FROM events
WHERE created_at >= '2025-10-12T00:00:00'
AND created_at < '2025-10-13T00:00:00';
這種方法高效且獨立於資料庫。它還允許資料庫使用created_at列上的索引,即使表變得很大,也能保持查詢速度很快。
或者,如果我們希望更明確地了解範圍邊界,我們可以將該方法定義為:
List<Event> findByCreatedAtGreaterThanEqualAndCreatedAtLessThan(
LocalDateTime start,
LocalDateTime end
);
此方法產生的結果與Between版本相同,但更清楚地表明下限是包含的( >= ),上限是排除的( < ):
List<Event> results = eventRepository
.findByCreatedAtGreaterThanEqualAndCreatedAtLessThan(startOfDay, endOfDay);
assertEquals(3, results.size());
5. 使用 JPQL @Query和資料庫函數
使用LocalDate值查詢LocalDateTime欄位的另一種方法是使用 JPQL @Query和資料庫特定函數。某些資料庫(例如 MySQL、PostgreSQL 和 H2)提供了內建的DATE()函數,該函數僅從時間戳中提取日期部分,而忽略時間部分。
這使我們能夠直接將提取的日期與我們的LocalDate值進行比較。
在這種方法中,我們使用@Query註解定義自訂查詢:
@Query("SELECT e FROM Event e WHERE FUNCTION('DATE', e.createdAt) = :date")
List<Event> findByDate(@Param("date") LocalDate date);
在這個範例中, FUNCTION('DATE', e.createdAt)告訴 JPA 呼叫底層資料庫中的DATE()函數。這會將createdAt中儲存的時間戳記轉換為純日期(例如,將2025-10-12T14:30:45轉換為2025-10-12 )。
由於條件的兩邊都是DATE值,因此比較變得簡單。
要執行查詢,我們只需傳入一個LocalDate值:
LocalDate date = LocalDate.of(2025, 10, 12);
List<Event> events = eventRepository.findByDate(date);
assertEquals(3, results.size());
這將產生類似於以下內容的 SQL 查詢:
SELECT * FROM events
WHERE DATE(created_at) = '2025-10-12';
這種方法簡潔,通常比基於範圍的方法更容易閱讀。但是,並非所有資料庫都支援相同的DATE()函數;例如,Oracle 使用的是TRUNC() 。
此外,直接在DATE(created_at)欄位上使用函數會阻止資料庫有效地使用索引。
6. 使用 Criteria API 進行動態查詢
有時我們希望在運行時建立查詢,而不是硬編碼儲存庫方法。例如,我們可能有一個搜尋畫面,使用者選擇一個日期,我們需要取得所有createdAt在該日期的記錄。
Criteria API 讓我們以程式設計方式執行此操作,同時仍然遵循與基於範圍的方法相同的邏輯:
@Repository
public class EventCriteriaRepository {
@PersistenceContext
private EntityManager entityManager;
public List<Event> findByCreatedDate(LocalDate date) {
LocalDateTime startOfDay = date.atStartOfDay();
LocalDateTime endOfDay = date.plusDays(1).atStartOfDay();
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Event> cq = cb.createQuery(Event.class);
Root<Event> root = cq.from(Event.class);
cq.select(root).where(
cb.between(root.get("createdAt"), startOfDay, endOfDay)
);
return entityManager.createQuery(cq).getResultList();
}
}
在範例中,我們首先將LocalDate轉換為兩個LocalDateTime邊界:
startOfDay → 00:00:00 of the selected date
endOfDay → 00:00:00 of the next day
然後,我們使用CriteriaBuilder建立一個查詢,選擇所有createdAt位於這兩個時間戳記之間的Event記錄。這實際上與運行以下命令相同:
SELECT * FROM events
WHERE created_at >= '2025-10-12T00:00:00'
AND created_at < '2025-10-13T00:00:00';
為了執行此操作,我們可以簡單地傳遞LocalDate值:
LocalDate date = LocalDate.of(2025, 10, 12);
List<Event> results = eventCriteriaRepository.findByCreatedDate(date);
assertEquals(3, results.size());
Criteria API 使所有內容保持類型安全,這在我們以後想要添加更多謂詞(例如,按事件名稱或狀態進行過濾)同時仍然正確處理LocalDate和LocalDateTime轉換時很有用。
7. 使用原生 SQL 查詢
在某些情況下,我們可能已經有一個用 SQL 編寫的複雜查詢,或者我們需要使用 JPQL 不支援的特定於資料庫的函數。 Spring Data JPA 允許我們使用@Query註解並設定nativeQuery=true來編寫 SQL 語句:
public interface EventRepository extends JpaRepository<Event, Long> {
@Query(
value = "SELECT * FROM events " +
"WHERE created_at >= :startOfDay " +
"AND created_at < :endOfDay",
nativeQuery = true
)
List<Event> findByDateRangeNative(
@Param("startOfDay") LocalDateTime startOfDay,
@Param("endOfDay") LocalDateTime endOfDay
);
}
nativeQuery = true標誌告訴 Spring Data JPA,此查詢應直接傳送至資料庫引擎,無需轉換。透過使用原生 SQL,我們可以完全控制執行的語句。
8. 結論
在本文中,我們學習如何在 JPA 中使用LocalDate值查詢LocalDateTime欄位。我們探索了多種方法,例如簡單的範圍查詢、JPQL 函數和動態 Criteria API 查詢。
與往常一樣,原始碼可在 GitHub 上取得。