 Java, Spring and Web Development tutorials  1. Introduction
When working with databases in JPA, it’s common to store timestamps in a database table, such as createdAt or updatedAt. But sometimes we only have a LocalDate, such as when a user selects a date from a calendar. In that case, we may want to find all records that fall on that date, regardless of their time.
If we try to compare a LocalDateTime field directly with a LocalDate value, the query won’t match anything because the two data types are not the same. In this tutorial, we’ll explore several ways to query a LocalDateTime field using a LocalDate value in JPA.
2. Understanding the Core Problem
Suppose we have an entity with a field of type LocalDateTime:
@Entity
public class MyClass {
@Id
private UUID id;
private LocalDateTime createdAt;
// Getters and setters
}
And a repository defined like this:
@Service
public interface IRepository extends CrudRepository<MyClass, UUID> {
void deleteByCreatedAt(LocalDate createdAt);
}
At first glance, this looks valid, as the method name follows Spring Data conventions, and createdAt seems to match the field name. However, when we call it with a plain LocalDate:
repository.deleteByCreatedAt(LocalDate.of(2024, 01, 15));
JPA throws an exception:
org.springframework.dao.InvalidDataAccessApiUsageException:
Parameter value [2024-01-15] did not match expected type [java.time.LocalDateTime (n/a)];
This error happens because the createdAt column in the database is of type LocalDateTime, but the repository method receives a LocalDate. Spring Data JPA doesn’t automatically convert between the two, since one includes a time component while the other does not.
A LocalDateTime contains both date and time information (e.g., 2024-01-15T14:30:45), while a LocalDate only contains date information (e.g., 2024-01-15).
So when we run a query like:
WHERE created_at = '2024-01-15'
The database looks for a timestamp that exactly matches that value — including the time. Since actual timestamps usually have hours, minutes, and seconds, this equality comparison finds no results.
To correctly find all records from that day, we need to look for all createdAt values that fall within the entire date range — from midnight (2024-01-15T00:00:00) up to but not including the next midnight (2024-01-16T00:00:00). This required us to convert the LocalDate into a LocalDateTime range.
3. Project Setup
To use JPA in a Spring Boot project, we need to include the spring-boot-starter-data-jpa dependency along with a database driver. In this tutorial, we’ll use H2 to illustrate our examples.
Add the following dependencies to our pom.xml file:
<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. Defining the Entity
Next, we’ll define a simple entity to represent our database table.
Let’s call it an Event. It will have an ID, a name, and a createdAt field of type LocalDateTime:
@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
}
This entity maps directly to a table named events in the database. The @Entity annotation tells JPA that this class should be treated as a managed entity, while the @Table(name = “events”) annotation specifies the exact table name to use.
The createdAt field uses the LocalDateTime type, which stores both the date and time of creation—for example, a value like 2025-10-12T14:35:20. The @PreUpdate annotations automatically handle timestamp population.
3.2. Preparing Sample Data
To make the examples and test cases easier to follow, let’s also prepare some sample data in an H2 data file. We can use the data.sql file under src/main/resources — Spring Boot will automatically execute it at startup.
Create a file named data.sql and add the following:
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');
To ensure it still runs after Hibernate has created the events table, we must add this line to our application.properties:
spring.jpa.defer-datasource-initialization=true
4. Query by Date Range
The most reliable way to query a LocalDateTime field using a LocalDate value is by performing a range query. Instead of comparing equality between the two types, we convert the LocalDate into a start and end boundary that represents the entire day.
This means we’ll fetch all records where createdAt is greater than or equal to the start of the day, and less than the start of the next day.
We start by defining a query method in our JPA repository:
public interface EventRepository extends JpaRepository<Event, Long> {
List<Event> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
}
This method automatically generates a query to fetch all records where createdAt falls between the two given timestamps.
Now, we can easily query events for a specific date by converting a LocalDate into a range of LocalDateTime values:
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());
In this example, the startOfDay represents midnight (2025-10-12T00:00:00) and the endOfDay represents midnight of the next day (2025-10-13T00:00:00).
By using these two boundary values, the query captures all events created on October 12th, no matter what the exact time was.
Under the hood, Spring Data JPA generates an SQL query similar to this:
SELECT * FROM events
WHERE created_at >= '2025-10-12T00:00:00'
AND created_at < '2025-10-13T00:00:00';
This method is efficient and database-independent. It also allows the database to make use of indexes on the created_at column, which keeps the query fast even when the table grows large.
Alternatively, if we prefer to be more explicit about the range boundaries, we can define the method as:
List<Event> findByCreatedAtGreaterThanEqualAndCreatedAtLessThan(
LocalDateTime start,
LocalDateTime end
);
This method produces the same result as the Between version, but makes it clearer that the lower bound is inclusive (>=) and the upper bound is exclusive (<):
List<Event> results = eventRepository
.findByCreatedAtGreaterThanEqualAndCreatedAtLessThan(startOfDay, endOfDay);
assertEquals(3, results.size());
5. Using JPQL @Query and Database Functions
Another way to query a LocalDateTime field using a LocalDate value is by using JPQL @Query and database-specific functions. Some databases like MySQL, PostgreSQL, and H2 provide a built-in DATE() function that extracts only the date part from a timestamp, ignoring the time portion.
This allows us to directly compare the extracted date with our LocalDate value.
In this approach, we define a custom query using the @Query annotation:
@Query("SELECT e FROM Event e WHERE FUNCTION('DATE', e.createdAt) = :date")
List<Event> findByDate(@Param("date") LocalDate date);
In this example, FUNCTION(‘DATE’, e.createdAt) tells JPA to call the DATE() function in the underlying database. This converts the timestamp stored in createdAt into a plain date (for example, converting 2025-10-12T14:30:45 into 2025-10-12).
The comparison then becomes straightforward because both sides of the condition are DATE values.
To execute the query, we simply pass in a LocalDate value:
LocalDate date = LocalDate.of(2025, 10, 12);
List<Event> events = eventRepository.findByDate(date);
assertEquals(3, results.size());
This will generate a SQL query similar to:
SELECT * FROM events
WHERE DATE(created_at) = '2025-10-12';
This approach is concise and often easier to read than the range-based method. However, not all databases support the same DATE() functions; for example, Oracle uses TRUNC() instead.
Furthermore, using a function directly on the column DATE(created_at) prevents the database from using indexes efficiently.
6. Using the Criteria API for Dynamic Queries
Sometimes we want to build queries at runtime rather than hard-coding repository methods. For example, we might have a search screen where users pick a date, and we need to fetch all records whose createdAt falls on that date.
The Criteria API lets us do this programmatically while still following the same logic as our range-based approach:
@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();
}
}
In the example, we first convert the LocalDate into two LocalDateTime boundaries:
startOfDay → 00:00:00 of the selected date
endOfDay → 00:00:00 of the next day
Then we use the CriteriaBuilder to create a query that selects all Event records whose createdAt lies between those two timestamps. This is effectively the same as running:
SELECT * FROM events
WHERE created_at >= '2025-10-12T00:00:00'
AND created_at < '2025-10-13T00:00:00';
To execute this, we can simply pass the LocalDate value:
LocalDate date = LocalDate.of(2025, 10, 12);
List<Event> results = eventCriteriaRepository.findByCreatedDate(date);
assertEquals(3, results.size());
The Criteria API keeps everything type-safe, which is useful when we want to add more predicates later (for example, filtering by event name or status) while still handling the LocalDate and LocalDateTime conversion correctly.
7. Using Native SQL Query
In some cases, we may already have a complex query written in SQL, or we need to use a database-specific function that JPQL doesn’t support. Spring Data JPA allows us to write SQL statements using the @Query annotation with nativeQuery=true:
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
);
}
The nativeQuery = true flag tells Spring Data JPA that this query should be sent directly to the database engine without translation. By using native SQL, we get full control over the executed statement.
8. Conclusion
In this article, we learned how to query LocalDateTime fields using a LocalDate value in JPA. We explored multiple approaches, such as simple range queries, JPQL functions, and dynamic Criteria API queries.
As always, the source code is available over on GitHub. The post How to Query JPA LocalDateTime Field With a LocalDate Value first appeared on Baeldung.
Content mobilized by FeedBlitz RSS Services, the premium FeedBurner alternative. |