Few things that every Spring boot developer should know: JPA Edition
1. Using @QueryHint annotation —
In Spring, you can set default timeout for the SQL queries by adding below property in application.properties file.
spring.jpa.properties.javax.persistence.query.timeout=5000
When you use the above property, all the queries will timeout at 5 seconds. But what will you do if you want to use a different timeout for a query? That’s where @QueryHint annotation is helpful. You can use this annotation to set timeouts on query level.
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u")
@QueryHint(name = "javax.persistence.query.timeout", value = "3000")
List<User> findAllWithTimeout();
}
Query hints is actually an SQL concept. Query hints provide instructions or suggestions to the database engine on how to optimize or execute a particular query. In the context of Spring, Spring Data JPA provides support for using query hints in combination with JPA (Java Persistence API) to specify additional directives for query execution. Spring Data JPA allows you to add query hints to your JPQL (Java Persistence Query Language) or native SQL queries.
Apart from setting timeouts, you can also use @QueryHint annotation to enable caching, specify locking strategy, or to even invalidate cache. You can also set multiple hints using @QueryHints annotation. Below are some code examples, But please make sure to read the official documentation for deep understanding.
@QueryHints(value = @QueryHint(name = "org.hibernate.cacheable", value = "true"))
@Query("SELECT u FROM User u")
List<User> findAllCacheable();
@QueryHints(value = {
@QueryHint(name = "javax.persistence.lock.timeout", value = "2000"),
@QueryHint(name = "javax.persistence.lock.mode", value = "PESSIMISTIC_WRITE")
})
@Query("SELECT u FROM User u WHERE u.id = :id")
User findUserForUpdate(@Param("id") Long id);
@QueryHints(value = @QueryHint(name = "javax.persistence.cache.storeMode", value = "REFRESH"))
@Query("SELECT u FROM User u WHERE u.id = :id")
User findUserWithCacheRefresh(@Param("id") Long id);
2. JPA Auditing and Audit Aware —
Spring Data JPA provides auditing capabilities that automatically capture information about entity creations, modifications, and deletions. By adding appropriate annotations like @CreatedBy
, @LastModifiedBy
, @CreatedDate
, and @LastModifiedDate
to your entity classes, you can track who modified the entity and when. Spring Data JPA will automatically populate these fields based on the current user and timestamp. Auditing is especially useful for maintaining an audit trail and implementing security measures. You can use @EnableJpaAuditing
annotation to enable the JPA Auditing.
@Entity
@EntityListeners(AuditingEntityListener.class)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@CreatedDate
private LocalDateTime createdAt;
@CreatedBy
private String createdBy;
@LastModifiedDate
private LocalDateTime lastModifiedAt;
@LastModifiedBy
private LocalDateTime lastModifiedBy;
// ...
}
Probably many of you already know about @CreatedDate
, and @LastModifiedDate
because they work out of the box without having to do any special configuration. Most of the projects don’t make use of @CreatedBy
, @LastModifiedBy
annotations because they don’t work out of the box. To make these work, we need define an AuditorAware
bean that returns the currently logged-in user or any other source of user information.
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
public class JpaAuditingConfig {
@Bean
public AuditorAware<String> auditorAware() {
return new HttpHeaderAuditorAware();
}
}
public class HttpHeaderAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
// Retrieve the userId from the HTTP header of the current request
// Replace "X-UserId" with the appropriate header name
String userId = HttpServletRequestUtil.getCurrentRequest().getHeader("X-UserId");
return Optional.ofNullable(userId);
}
}
public class HttpServletRequestUtil {
public static HttpServletRequest getCurrentRequest() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
return requestAttributes.getRequest();
}
throw new IllegalStateException("No current HttpServletRequest found");
}
}
In this implementation, we use HttpServletRequestUtil
(a custom utility class) to retrieve the current HTTP request and access the "X-UserId" header (replace it with the appropriate header name used in your application). If the header value is present, it will be returned as the current user. Otherwise, Optional.empty()
is returned. This way Spring Data JPA will be able to detect current logged in user, and will be able to populate fields that are marked with @CreatedBy
, @LastModifiedBy
annotations. You can choose to not use this approach, and manually set the fields which will also work perfectly. In the end you need to decide what will benefit you and the team.
3. Using EnitityGraph for better query optimization —
In JPA, when you retrieve an entity from the database, it’s common to have associations (relationships) between entities. By default, JPA uses lazy loading for associations, which means that the associated entities are fetched from the database only when accessed for the first time. This lazy loading strategy can lead to additional database queries (N+1 problem) if you access multiple associations, resulting in suboptimal performance.
The EntityGraph
feature in JPA allows you to define a fetch plan for entity associations, specifying which associations should be eagerly loaded (fetched) along with the main entity in a single query. This fetch plan is defined as a graph of entity associations, and it provides an optimized way to fetch multiple entities and their associations in a single database query, reducing the number of round-trips and enhancing performance.
@Entity
@NamedEntityGraph(name = "postWithComments",
attributeNodes = @NamedAttributeNode(value = "comments"))
public class Post {
// ...
}
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
@EntityGraph(value = "postWithComments", type = EntityGraph.EntityGraphType.FETCH)
Optional<Post> findById(Long id);
}
In the above example, the postWithComments
NamedEntityGraph
is defined for the Post
entity, specifying that the comments
association should be eagerly fetched. The @EntityGraph
annotation is applied to the repository method, specifying the EntityGraph
to be used. In this case, the postWithComments
EntityGraph
is used, and the type
attribute is set to FETCH
to indicate that the specified associations should be fetched eagerly.
You can use annotations like @ManyToOne(fetch = FetchType.EAGER)
or @OneToMany(fetch = FetchType.EAGER)
for fetching associations eagerly in JPA. Using these annotations for eager fetching applies the fetch strategy globally to all queries involving the entity or association. This means that every query involving that entity or association will eagerly fetch the associated data. This can lead to unnecessary data retrieval and performance issues if the association is not always needed. So it’s upto you to decide which approach suits you better.
4. Using Performance Monitoring Libraries—
You can utilize performance monitoring libraries such as Micrometer or Spring Boot Admin to collect and visualize application-level metrics, including JPA-related metrics. These libraries integrate with Spring Boot and provide a way to monitor various aspects of your application, including JPA performance. Here’s an example using Micrometer:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
Once the dependency is added, you can configure and expose JPA-related metrics through an endpoint. For example, with Micrometer and Prometheus, you can add the following configuration to expose JPA query metrics:
management:
endpoints:
web:
exposure:
include: hibernate.statistics
This configuration exposes the Hibernate statistics as an endpoint (/actuator/hibernate.statistics
), which can be accessed to gather JPA performance metrics. This will allow you to monitor the performance of JPA queries, track execution times, and gather other relevant statistics to optimize and troubleshoot your JPA-related operations in a Spring application. This is actually a bigger topic in general, you can find other sources in internet to understand other approaches, including spring boot admin.
These approaches allow you to monitor the performance of JPA queries, track execution times, and gather other relevant statistics to optimize and troubleshoot your JPA-related operations in a Spring application.
Thank you for reading this story so far, if you like then please clap and share with your friends and colleagues.
If you have knowledge, let others light their candles in it. — Margaret Fuller