Mbkuae Stack

5 Essential Techniques for Querying Date Ranges in Hibernate

Learn five proven methods to query records between two dates using Hibernate, including HQL, Criteria API, and Native SQL with best practices.

Mbkuae Stack · 2026-05-18 16:20:34 · Finance & Crypto

Querying data within a specific date range is a fundamental task in enterprise applications—whether you're generating monthly reports, filtering recent activity logs, or retrieving orders for a particular period. Hibernate offers multiple ways to handle temporal queries, each with its own strengths and pitfalls. In this listicle, we'll explore five key techniques for querying records between two dates: from using HQL's BETWEEN operator to leveraging the Criteria API and Native SQL. You'll learn not only how to write these queries but also how to avoid common logical errors, such as missing records at the boundaries. Let's dive in.

  1. Understanding Hibernate's Date-Time Support
  2. Using HQL with the BETWEEN Keyword
  3. Mastering Half-Open Intervals with Comparison Operators
  4. Dynamic Queries with the Criteria API
  5. When to Use Native SQL for Date Ranges

1. Understanding Hibernate's Date-Time Support

Before writing any date-range query, it's crucial to know how Hibernate handles date-time types. In modern versions (Hibernate 5+), Java 8's java.time types—like LocalDateTime, LocalDate, and Instant—are supported natively. This means you can map them directly in your entity without extra annotations. For example, an Order entity might have a creationDate field of type LocalDateTime:

5 Essential Techniques for Querying Date Ranges in Hibernate
Source: www.baeldung.com
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String trackingNumber;
    private LocalDateTime creationDate;
    // getters and setters
}

If you're still using legacy java.util.Date, you'll need the @Temporal annotation to specify whether to store date, time, or both. Modern setup simplifies your code and reduces the risk of time zone issues. Always prefer Java 8 date-time types when possible for cleaner predicates and better accuracy in range queries.

2. Using HQL with the BETWEEN Keyword

The most straightforward approach in Hibernate Query Language (HQL) is the BETWEEN operator. It allows you to write concise, readable queries like:

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();

While this looks clean, it hides a subtle trap: BETWEEN is inclusive on both ends. If you're working with LocalDateTime and you want all orders on January 31, setting endDate to 2024-01-31T00:00 will miss every order after midnight. To include the entire day, you'd have to manually set the time to the last millisecond (23:59:59.999). This fragile calculation is error-prone. Therefore, BETWEEN is best reserved for situations where both boundaries are inclusive—e.g., when querying whole dates (with LocalDate) or when the time part is always zero. For most real-world scenarios, a half-open interval is a safer bet.

3. Mastering Half-Open Intervals with Comparison Operators

When querying calendar boundaries like full days or months, the recommended pattern is a half-open interval: inclusive on the lower bound and exclusive on the upper bound. Use >= for the start and < for the end. For example, to get all orders from January 2024:

String hql = "FROM Order o WHERE o.creationDate >= :startDate AND o.creationDate < :endDate";
LocalDateTime start = LocalDateTime.of(2024, 1, 1, 0, 0);
LocalDateTime end = LocalDateTime.of(2024, 2, 1, 0, 0);
List<Order> orders = session.createQuery(hql, Order.class)
  .setParameter("startDate", start)
  .setParameter("endDate", end)
  .getResultList();

Notice we pass February 1 as the exclusive end—this cleanly captures every millisecond of January without manual time calculations. This pattern works for any granularity: hours, days, months, or years. It's also database-agnostic and avoids edge cases with time zones and fractional seconds. Adopting half-open intervals as your default for date-range queries will eliminate a whole class of off-by-one errors.

5 Essential Techniques for Querying Date Ranges in Hibernate
Source: www.baeldung.com

4. Dynamic Queries with the Criteria API

When your date-range query depends on runtime conditions—like optional filters or user input—the Criteria API offers a type-safe, programmatic alternative to HQL. You can build the query step by step:

CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<Order> cr = cb.createQuery(Order.class);
Root<Order> root = cr.from(Order.class);
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));
}
cr.select(root).where(predicates.toArray(new Predicate[0]));
List<Order> orders = session.createQuery(cr).getResultList();

This approach shines when you need to conditionally add date boundaries or combine them with other filters. It avoids string concatenation and is fully refactoring-friendly. The same half-open interval logic applies: use greaterThanOrEqualTo for start and lessThan for end. For pagination, you can wrap it with setFirstResult() and setMaxResults(). The Criteria API also makes it easy to switch between AND/OR logic, perfect for complex search forms.

5. When to Use Native SQL for Date Ranges

While HQL and Criteria cover most needs, sometimes you require database-specific date functions or non-standard SQL syntax—for example, using DATE_TRUNC in PostgreSQL or converting time zones. In such cases, Native SQL queries let you leverage the full power of the underlying database while still mapping results to entities:

String sql = "SELECT * FROM orders WHERE creation_date >= ? AND creation_date < ?";
List<Order> orders = session.createNativeQuery(sql, Order.class)
  .setParameter(1, startDate)
  .setParameter(2, endDate)
  .getResultList();

Use Native SQL sparingly—it ties your code to a particular database vendor and bypasses Hibernate's caching and lazy-loading optimizations. However, it's invaluable for performance-critical queries that need to exploit database-specific indexing or window functions. Always validate that the same result cannot be achieved with HQL or Criteria before dropping down to native.

Conclusion: Querying records between two dates in Hibernate doesn't have to be a headache. By understanding date-time types, preferring half-open intervals over BETWEEN, leveraging the Criteria API for dynamic conditions, and reserving Native SQL for special cases, you can write robust, maintainable temporal queries. These five techniques will help you avoid common pitfalls and keep your code clean across any enterprise application.

Recommended