Ohhnews

分类导航

$ cd ..
Baeldung原文

在Hibernate中实现日期范围查询的三种方法

#hibernate#java#数据库查询#日期处理#后端开发

[LOADING...]

1. 概述

在大多数企业级应用中,查询特定时间范围内的数据是一项核心需求。无论是生成月度财务报表,还是筛选过去 24 小时的日志,Hibernate 都提供了多种处理此类时间查询的方法。

在本教程中,我们将探讨如何使用 HQLCriteria API 和原生 SQL 来查询两个日期之间的记录。

2. 设置

为了演示这些操作,我们先定义一个 Order(订单)实体。虽然旧版本的 Hibernate 需要特定的日期注解,但现代版本(Hibernate 5+)可以原生处理 Java 8 的 java.time 类型

$ java
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String trackingNumber;
    private LocalDateTime creationDate;
    // Getter 和 Setter 方法
}

如果我们仍在使用传统的 java.util.Date,则需要使用 @Temporal 注解来指定存储的是日期、时间还是两者皆有:

$ java
@Temporal(TemporalType.TIMESTAMP)
private Date legacyCreationDate;

3. 使用 Hibernate 查询语言 (HQL)

HQL 是 Hibernate 中编写日期范围查询最常用的方式。它具有良好的可读性,跨数据库可移植,并且允许我们直接操作实体模型,而不是原始的 SQL 表。

3.1. 使用 BETWEEN 关键字

BETWEEN 运算符是查询特定范围内记录最直接的方法。它是两端包含的,这意味着落在 startDateendDate 上的记录都会被包含在结果中

$ java
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 下的订单在技术上“大于”结束参数,从而被排除在结果之外。

为了使用 BETWEEN 捕获最后一天,我们必须手动将时间设置为该天的最后一毫秒(23:59:59.999)。为了避免这种脆弱的手动计算,更稳健的模式是使用比较运算符创建“半开区间”。

3.2. 使用比较运算符

当我们查询日历边界(如全天或整月)时,最安全的模式是使用半开区间:下界包含,上界不包含。这意味着我们对开始时间使用 >=,对结束时间使用 <

假设我们想要 2024 年 1 月的所有订单。与其费力计算 1 月 31 日的 23:59:59,不如简单地使用 2 月 1 日作为排除性的 endDate

$ java
LocalDateTime startDate = LocalDateTime.of(2024, 1, 1, 0, 0, 0);
LocalDateTime endDate = LocalDateTime.of(2024, 2, 1, 0, 0, 0); // 不包含
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 上的记录:

$ java
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 中相同的半开区间模式:

$ java
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 的优势就体现出来了。我们可以构建一个 Predicate 对象列表,并仅应用用户提供的那些条件:

$ java
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

$ java
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()

$ java
// 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 上找到。