Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 36

Custom Queries with Spring Data JPA

Recommended resources

Hello! Welcome back to our journey into backend development.

In our last lesson, we saw how to create JpaRepository interfaces to get a full suite of CRUD (Create, Read, Update, Delete) methods without writing any implementation code. This is incredibly powerful for basic database operations. However, real-world applications often require more specific queries. For instance, how would you find a product by its name? Or search for all products within a certain price range? The default findAll() and findById() methods won't be enough.

This is where today's topic comes in. Our goal is to learn how to write custom query methods using Spring Data JPA's naming conventions. This feature, often called "derived queries," allows Spring to automatically generate the necessary database queries simply by analyzing the names of the methods you declare in your repository interface. It's a powerful way to extend your data access layer while keeping your code clean and readable.

Let's dive in and see how we can "teach" our repositories to perform more complex searches.

1. The Anatomy of a Derived Query Method

The magic of derived queries lies in a specific naming structure that Spring Data JPA understands. By following this convention, you can express surprisingly complex query logic.

A derived query method name is composed of two main parts, separated by the keyword By:

[Introducer]By[Criteria]

  • Introducer: This prefix tells Spring Data JPA what the query should do. Common introducers are find, get, read, and query, which all retrieve entities. Another useful introducer is count, which returns the number of matching entities.
  • Criteria: This part describes the conditions for the query. It's formed by combining the names of your entity's properties (like name or price) with special keywords (like Containing or LessThan).

For example, in our ProductRepository, a method to find products by their name would be:

List<Product> findByName(String name);

Here, find is the introducer, and Name is the criteria, which corresponds to the name property in our Product entity. When you call this method, Spring Data JPA automatically generates and executes a query similar to SELECT * FROM products WHERE name = ?.

To get a formal introduction to this concept, the following resource provides a great starting point.

Spring Data JPA Derived Queries

The article "Spring Data JPA Derived Queries" from CodingNomads clearly explains the fundamental concept and structure of derived query methods.

Please read the sections "What is a Derived Query Method?" and "How are Derived Query Methods Structured?". This will reinforce the core idea of the introducer and criteria separated by the By keyword.


2. Building Criteria with Keywords

The real power of derived queries comes from the rich set of keywords you can use in the criteria part of the method name. Let's explore the most common ones.

We'll use our Product entity as a reference, assuming it has properties like name (String), price (BigDecimal), and dateAdded (LocalDate).

Equality and Similarity Keywords

These are for exact matches, non-matches, and pattern matching on strings.

Keyword Example Method Generated SQL (Conceptual)
(none) or Is findByName(String name) ... WHERE name = ?
IsNot findByNameIsNot(String name) ... WHERE name <> ?
IsNull / IsNotNull findByDescriptionIsNull() ... WHERE description IS NULL
StartingWith findByNameStartingWith(String prefix) ... WHERE name LIKE 'prefix%'
EndingWith findByNameEndingWith(String suffix) ... WHERE name LIKE '%suffix'
Containing findByNameContaining(String infix) ... WHERE name LIKE '%infix%'
IgnoreCase findByNameContainingIgnoreCase(String infix) ... WHERE UPPER(name) LIKE UPPER('%infix%')

Notice that for keywords like StartingWith or Containing, you don't need to add the % wildcard characters to the input string; Spring Data JPA handles that for you. The IgnoreCase modifier is particularly useful for user-driven search functionality.

Comparison Keywords

These are essential for working with numerical values and dates.

Keyword Example Method Generated SQL (Conceptual)
LessThan findByPriceLessThan(BigDecimal price) ... WHERE price < ?
GreaterThan findByPriceGreaterThan(BigDecimal price) ... WHERE price > ?
LessThanEqual findByStockLessThanEqual(int stock) ... WHERE stock <= ?
GreaterThanEqual findByStockGreaterThanEqual(int stock) ... WHERE stock >= ?
Between findByPriceBetween(BigDecimal min, BigDecimal max) ... WHERE price BETWEEN ? AND ?
After / Before findByDateAddedAfter(LocalDate date) ... WHERE date_added > ?
In findByNameIn(Collection<String> names) ... WHERE name IN (?, ?, ...)

The Between and In keywords are very powerful for building filters. For example, findByPriceBetween could power a price range slider on a website, and findByNameIn could retrieve a specific list of products based on a user's selection.

The following resource provides excellent examples for all these keywords.

Derived Query Methods in Spring Data JPA Repositories

Baeldung's article on derived queries is a comprehensive reference. It covers a wide range of keywords with clear code examples.

Please read sections 4 ("Equality Condition Keywords"), 5 ("Similarity Condition Keywords"), and 6 ("Comparison Condition Keywords"). Pay attention to the variety of keywords available, especially Between, In, After, and Before, which are very practical.

Test your understanding!

Imagine our Product entity has a boolean property isAvailable. How would you write a derived query method to find all products that are currently available? What about a method to find all products with a stock level of less than 10?

Show answer
  1. To find available products: List<Product> findByIsAvailableTrue();
  2. To find products with low stock: List<Product> findByStockLessThan(int stockLevel); (You would then call it with the value 10).

3. Combining Conditions and Advanced Techniques

Derived queries can be much more than single-condition lookups. You can combine criteria, query into related entities, and control the result set.

Combining with And and Or

You can chain multiple criteria together using And and Or to build more complex logic. The order of method parameters must match the order of the criteria in the method name.

  • findByIsAvailableTrueAndPriceLessThan(BigDecimal maxPrice): Finds available products cheaper than a given price.
  • findByNameContainingOrDescriptionContaining(String query, String query): A basic search that looks for a term in either the name or the description.

Querying Nested Properties

What if you want to find all products belonging to a category with a specific name? In our data model, Product has a Category object. You can "traverse" this relationship using an underscore (_).

// Assuming Product has a 'Category category' field, and Category has a 'String name' field
List<Product> findByCategory_Name(String categoryName);

This is extremely useful for filtering based on the properties of related entities without having to fetch them first.

Limiting and Sorting Results

You can control the sorting and size of your results directly in the method name.

  • Sorting: Use the OrderBy keyword followed by the property name and an optional direction (Asc for ascending, Desc for descending).
    • findByCategory_NameOrderByPriceAsc(String categoryName): Finds products in a category, sorted by price from lowest to highest.
  • Limiting: Use the Top or First keywords to limit the number of results. You can specify a number, like findTop5....
    • findFirstByOrderByDateAddedDesc(): Returns the single newest product.
    • findTop5ByOrderByPriceDesc(): Returns the 5 most expensive products.

This summary table provides a fantastic visual overview of how these keywords map to query logic.

Spring Data JPA Derived Query Methods Keywords
This table illustrates how derived query method names using keywords like `And`, `Or`, `Between`, and `LessThan` are translated by Spring Data JPA into corresponding JPQL (Java Persistence Query Language) statements.

4. Practical Application

Now, let's put this into practice. In your project, you should have a ProductRepository interface. Try adding the following method declarations to it:

public interface ProductRepository extends JpaRepository<Product, Long> {

    // Find products by name, ignoring case
    List<Product> findByNameContainingIgnoreCase(String name);

    // Find products within a specific price range
    List<Product> findByPriceBetween(BigDecimal startPrice, BigDecimal endPrice);

    // Find the 10 most recently added products
    List<Product> findTop10ByOrderByDateAddedDesc();
    
    // Find products belonging to a specific category by the category's name
    List<Product> findByCategory_Name(String categoryName);
    
    // Count how many products are in a given category
    long countByCategory_Name(String categoryName);

}

You don't need to write any implementation! Just declare these methods. You can then inject your ProductRepository into a service or a CommandLineRunner and call them to see them in action.


Conclusion

Today you've learned one of Spring Data JPA's most productive features. By adhering to a clear naming convention, you can create a wide range of custom queries without writing a single line of SQL or JPQL.

Key Takeaways:

  • Derived queries follow the structure [Introducer]By[Criteria].
  • You can build criteria using keywords for equality (Is, IsNot), similarity (Containing), comparison (LessThan, Between), and more.
  • Conditions can be combined with And and Or.
  • You can query properties of related entities using the underscore character (e.g., findByCategory_Name).
  • Results can be sorted (OrderBy...Desc) and limited (findTop5...).

What's Next?

Derived queries are excellent for a majority of use cases. However, method names can become very long and unreadable for highly complex queries. What if your logic is too complicated to be expressed in a method name? In our next lesson, we will address this limitation by learning how to implement custom queries using the @Query annotation with JPQL. This will give you full control to write any query you can imagine.