Optimizing Performance with Spring Data JPA

Avinash Kumar Singh
11 min readAug 29, 2023

--

Welcome to the world of streamlined and efficient data access in Java applications — welcome to Spring Data JPA. In the realm of modern software development, managing and interacting with databases is a critical task, and Spring Data JPA emerges as a powerful solution. However, as with any tool, it’s important to use Spring Data JPA properly in order to get the best performance and efficiency.
Optimizing performance with Spring Data JPA involves several strategies aimed at improving database interaction efficiency, reducing unnecessary queries, and optimizing data retrieval and manipulation. Here are some tips to help you optimize performance when using Spring Data JPA:

1. Use Proper Indexing:

  • Analyze the query execution plans and identify the frequently used columns in WHERE, JOIN, and ORDER BY clauses. Create indexes on these columns to speed up data retrieval.
  • Avoid excessive indexing, as it can impact insert/update performance. Strike a balance between read and write operations.

Let’s go through an example:

Suppose you have a Spring Boot application that uses JPA to interact with a database containing information about books and their authors. You have two main entities: Book and Author.

@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String isbn;

@ManyToOne
@JoinColumn(name = "author_id")
private Author author;

// Constructors, getters, setters
}

@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;

// Constructors, getters, setters
}

Let’s say you frequently run a query to retrieve all books by a specific author. Without any indexes, the database would have to scan through the entire Book table to find the matching rows, which can be very slow as the table grows.

To address this, you can add an index on the author_id column in the Book table, which is used for the join with the Author table.

@Entity
public class Book {
// ...

@ManyToOne
@JoinColumn(name = "author_id")
@org.hibernate.annotations.Index(name = "author_id_index") // Adding an index
private Author author;

// ...
}

By creating this index, querying for books by a specific author becomes much faster because the database can quickly locate the relevant rows based on the indexed column.

Remember that while indexing can significantly improve query performance, it’s important not to over-index, as indexes come with some overhead in terms of storage and update performance. You should analyze your application’s query patterns and create indexes on columns that are frequently queried or involved in joins.

In summary, “Use Proper Indexing” in Spring Boot JPA involves adding indexes to database columns that are frequently queried to optimize query performance. This practice can lead to significant improvements in application responsiveness and user experience.

2. Fetch Strategies:

“Fetch strategy” refers to the way in which related entities (associated entities) are loaded from the database when querying for an entity. Fetch strategies determine whether related entities should be fetched eagerly or lazily.

  • Choose appropriate fetch strategies for associations (e.g., lazy loading) to prevent unnecessary loading of related entities.
  • Use explicit JOIN FETCH clauses in JPQL queries to fetch related entities in a single query when needed.

Let’s consider an example with two entities: Author and Book. An author can have multiple books, and we’ll look at how to manage the fetch strategy for this relationship.

@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;

@OneToMany(mappedBy = "author", fetch = FetchType.LAZY) // Lazy fetch strategy
private List<Book> books = new ArrayList<>();

// getters and setters
}

@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String title;

@ManyToOne(fetch = FetchType.EAGER) // Eager fetch strategy
@JoinColumn(name = "author_id")
private Author author;

// getters and setters
}

In this example, we have set up the fetch strategies for the Author and Book entities:

  • The Author entity has a one-to-many relationship with the Book entity, and it uses lazy fetching for the books collection. This means that when you fetch an Author, its books collection will not be loaded until you actually access it in your code.
  • The Book entity has a many-to-one relationship with the Author entity, and it uses eager fetching for the author field. This means that when you fetch a Book, its associated Author will be fetched immediately.

Remember that choosing the right fetch strategy depends on your application’s requirements and usage patterns. Lazy fetching can lead to N+1 query issues if not managed properly, where each related entity triggers a separate database query. It’s important to analyze your application’s use cases and choose the appropriate fetch strategy accordingly.

3.Batch Fetching:

“Batch fetching” refers to a technique used to optimize database queries when dealing with associations between entities, especially one-to-many or many-to-many relationships. It aims to reduce the number of individual database queries performed when loading related entities, thus improving performance and minimizing the N+1 query problem.

  • Utilize batch fetching to fetch multiple entities in a single query, reducing the N+1 query problem.
  • Configure batch size using @BatchSize annotation or configuration properties.

Let’s consider a simplified e-commerce domain with two entities: Order and OrderItem. An order can have multiple order items. We'll focus on optimizing the fetching of order items using batch fetching.

1.Define Entities:

@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

// Other order attributes, getters, setters
}

@Entity
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY) // Use LAZY to avoid loading items eagerly
@JoinColumn(name = "order_id")
private Order order;

// Other order item attributes, getters, setters
}

In the OrderItem entity, we're using @ManyToOne to define the association with the Order entity. The fetch = FetchType.LAZY setting ensures that the association is loaded lazily, which is a good practice to avoid unnecessary eager loading.

2. Fetching Order Items with Batch Fetching:

To perform batch fetching, you can use the @BatchSize annotation from Hibernate (JPA's underlying implementation). This annotation specifies the batch size for fetching associations. It indicates that when loading entities, Hibernate should fetch a certain number of associated entities in a single query.

@Entity
@BatchSize(size = 10) // Set the batch size for batch fetching
public class Order {
// ...
}

In this example, we’ve set the batch size to 10 for the Order entity, meaning that when fetching orders, Hibernate will also fetch the associated order items in batches of 10.

3. Fetching Orders with Order Items:

Now, when you fetch Order entities using a JPA query, Hibernate will automatically use batch fetching to fetch the associated OrderItem entities in batches, reducing the number of queries executed.

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findAll();
}

In your service or controller class, you can use the OrderRepository to fetch orders:

@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;

public List<Order> getAllOrders() {
return orderRepository.findAll();
}
}

By setting the batch size appropriately and using batch fetching, you can efficiently retrieve Order entities along with their associated OrderItem collections, minimizing the number of database queries executed. This optimization becomes even more noticeable as the size of the dataset increases.

4.Caching:

Caching can significantly improve the performance of your application by reducing the number of database queries and increasing response times.

  • Enable caching with Spring Cache or third-party solutions like Ehcache or Caffeine to store frequently accessed data in memory, reducing database hits.
  • Use second-level caching to cache data across sessions. Spring Boot supports various caching providers.

Let’s go through a detailed explanation of caching in Spring Boot JPA with an example:

1. Cache Eviction:

Cached data might become stale over time. To handle this, Spring provides the @CacheEvict annotation, which you can use to remove items from the cache when certain conditions are met.

import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;

@Service
public class ProductService {

@Cacheable("products")
public Product getProductById(Long productId) {
// Simulate fetching data from the database
return databaseService.fetchProductById(productId);
}

@CacheEvict(value = "products", key = "#productId")
public void updateProduct(Long productId, Product updatedProduct) {
// Simulate updating data in the database
databaseService.updateProduct(productId, updatedProduct);
}
}

In this example, the updateProduct method is marked with @CacheEvict. This means that when this method is called, the corresponding entry in the "products" cache will be removed.

2.Cache Put:

If you want to manually put data into the cache, you can use the @CachePut annotation. This annotation is useful when you want to update the cached data as well as return the updated data.

import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;

@Service
public class ProductService {

@CachePut(value = "products", key = "#productId")
public Product updateProduct(Long productId, Product updatedProduct) {
// Simulate updating data in the database
databaseService.updateProduct(productId, updatedProduct);

return updatedProduct;
}
}

In this case, the updateProduct method both updates the data in the database and updates the cache with the new data.

5.Query Optimization:

When you use JPA with Spring Boot, you write Java code to interact with your database, and JPA translates these Java objects and operations into SQL queries to manipulate the underlying database. Query optimization becomes important to ensure that these generated SQL queries are efficient, minimize database load, and result in faster response times.

  • Write efficient JPQL or Criteria API queries that fetch only the required data. Avoid using SELECT *.
  • Use the appropriate projections (e.g., SELECT NEW or DTO projections) to fetch only necessary fields.

Here’s an example of how to use query optimization techniques in Spring Boot with JPA:

Let’s consider a simple scenario where you have an entity class Product representing products in an e-commerce application. Each product has an ID, name, price, and category. You also have a repository interface ProductRepository that extends the JpaRepository interface provided by Spring Data JPA.

  1. Optimized Query Execution:

Suppose you want to retrieve a list of products within a specific price range. You can utilize Spring Data JPA’s method naming conventions to create a query method that retrieves products within the desired price range. Spring Data JPA will automatically generate the appropriate SQL query based on the method name.

import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByPriceBetween(double minPrice, double maxPrice);
}

In this example, the method findByPriceBetween is used to retrieve products with prices within a given range. Spring Data JPA generates an optimized SQL query for this method.

6. Use Pagination & Sorting:

Paging and sorting are strategies employed to restrict the quantity of outcomes retrieved from a query and arrange them according to particular standards. Within Spring Data JPA, these approaches are executed through the utilization of the Pageable interface. This interface empowers you to define the size of each page, the criteria for sorting, and the number of the desired page.

To use paging and sorting in Spring Data JPA, you need to first define a Pageable object, which contains the page size, the sorting criteria, and the page number. Here's an example:

Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.by(sortBy).descending());

In this example, pageNumber is the current page number, pageSize is the number of results to return per page, and sortBy is the field to sort by in descending order.

Once you have a Pageable object, you can use it to query the database and get a page of results. Here's an example of using paging and sorting to get a page of results from a Product repository:

public Page<Product> findProducts(String keyword, Pageable pageable) {
return productRepository.findByName(keyword, pageable);
}

In this example, the findByName method is used to search for products by name, and the pageable parameter is used to specify the page size, the sorting criteria, and the page number.

Using paging and sorting can greatly improve the performance of your Spring Data JPA application, especially when dealing with large datasets. By limiting the number of results returned by a query and sorting them based on specific criteria, you can reduce the amount of data that needs to be processed and returned by the database, resulting in faster response times and improved performance.

7.Read-Only Operations:

Mark read-only transactions with @Transactional(readOnly = true) to improve database performance by bypassing unnecessary transaction-related overhead.

  • Mark read-only transactions with @Transactional(readOnly = true) to improve database performance by bypassing unnecessary transaction-related overhead.
@Service
@Transactional(readOnly = true)
public class OrderService {
@Autowired
private OrderRepository orderRepository;

public List<Order> getOrdersByStatus(OrderStatus status) {
return orderRepository.findOrdersByStatus(status);
}
}

8. Batch Operations:

  • Use batch processing for bulk inserts, updates, and deletes. Spring Data JPA supports batch operations through the saveAll() and deleteAllInBatch() methods.
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;

public List<Order> saveOrders(List<Order> orders) {
return orderRepository.saveAll(orders);
}
}

9.Avoid N+1 Query Problem:

The N+1 Query Problem is a common performance issue that occurs when using an Object-Relational Mapping (ORM) framework like Spring Boot JPA to retrieve data from a relational database. This problem arises when you fetch a collection of entities and, in order to access related data for each entity, the ORM generates additional queries for each entity. This leads to a significant increase in the number of database queries, resulting in poor performance and increased load on the database server.

Let’s break down the N+1 Query Problem with a detailed explanation and an example in the context of Spring Boot JPA:

Scenario:- Consider you have two entities: Author and Book, where an author can have multiple books. The relationship between them is one-to-many (one author has many books). Here are the entity classes:

@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// Other fields, getters, setters...
}

@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;

@ManyToOne
@JoinColumn(name = "author_id")
private Author author;
// Other fields, getters, setters...
}

Problem:- Let’s say you want to retrieve a list of authors along with their books. If you fetch the authors and then loop through them to access their books, JPA might generate separate queries for each author’s books. This results in one query to fetch the authors and N queries (where N is the number of authors) to fetch their respective books. Hence, the name “N+1” query problem.

Example:- Suppose you have two authors, and each author has three books. If you naively retrieve the authors and access their books, you’ll end up with 1 + 2 queries:

  1. Query to fetch authors.
  2. Query to fetch books for author 1.
  3. Query to fetch books for author 2.

This results in a total of 3 queries, where only one should have sufficed.

Solution:- To avoid the N+1 Query Problem, you can use the concept of “eager” or “lazy” loading. By default, JPA uses lazy loading for relationships. Lazy loading means related entities are loaded from the database only when you explicitly access them. To fetch related entities eagerly (all at once), you can use the @OneToMany(fetch = FetchType.EAGER) annotation on the collection.

However, eager loading might not always be the best option, as it can lead to other performance issues, like loading more data than necessary. An alternative is to use explicit queries with joins (JPQL or Criteria API) to fetch the data you need in a single query.

Using Eager Loading:

@Entity
public class Author {
// ...

@OneToMany(mappedBy = "author", fetch = FetchType.EAGER)
private List<Book> books;
// ...
}

Using Join Fetch Query:

@Repository
public interface AuthorRepository extends JpaRepository<Author, Long> {
@Query("SELECT DISTINCT a FROM Author a JOIN FETCH a.books")
List<Author> findAllWithBooks();
}

In this query, JOIN FETCH ensures that authors are retrieved along with their books in a single query.

By addressing the N+1 Query Problem, you can significantly improve the performance of your Spring Boot JPA applications by minimizing the number of database queries and reducing the load on the database server.

Conclusion

When using Spring Data JPA to interact with a database, it’s important to optimize performance to ensure efficient use of resources ,faster response times and a better performing backend.

Thank you for reading this article on optimizing performance with Spring Data JPA. I hope you found these techniques helpful for improving the performance of your Spring Boot applications.

If you enjoyed reading this article, please consider supporting me by liking, following, and leaving a comment below. Your feedback is valuable to me and will help me create more useful content in the future.

Please look this too — https://medium.com/@avi.singh.iit01/a-guide-to-api-versioning-in-spring-boot-d564595bba00

Thank you for your support!

--

--

Avinash Kumar Singh

Software Developer | Competitive Programmer | Backend Developer