Spring Data JPA Derived Query Methods Explained

Introduction

This tutorial explains derived query methods: Spring Data JPA's way of turning a method name such as findByAuthorAndPublishedYear into a database query, with no SQL or JPQL written by you. We will start with how Spring Data reads a method name and the parts it recognises. We will then declare a repository with a collection of methods - equality, comparisons, text matching, sorting, limiting, counting, existence checks and deletes - and run each of them on a small book catalogue. Next we will look at the SQL generated for a few of them, see the clear startup error that a wrong method name produces, and finish by listing the supported keywords and the point at which a name becomes too long and a different tool is better.

(This tutorial builds on Spring Data JPA Introduction: Entities and RepositoriesClick to read tutorial: Spring Data JPA Introduction: Entities and Repositories, with the same Book entity. Queries written by hand are the subject of @Query: JPQL and Native QueriesClick to read tutorial: Spring Data JPA @Query: JPQL and Native Queries.)

How Spring Data reads a method name

When the application starts, Spring Data parses the name of every method in a repository interface that it has no implementation for. A name is split into a few parts -
  • a subject that says what the method does: find...By, read...By, get...By, query...By (all the same), count...By, exists...By and delete...By (or remove...By); words between the verb and By are ignored, so findBooksByAuthor works, and Distinct, First and Top3 are recognised there;
  • a predicate after By: property names of the entity, each optionally followed by a keyword such as Between, LessThan or Containing, and joined with And and Or;
  • an optional ordering at the end: OrderByPriceDesc;
  • the method's parameters supply the values, in the order the keywords need them, and its return type says how to deliver the result: an entity, an Optional, a List, a long for counts, a boolean for existence.
The method name is checked against the entity at startup, so a typo fails immediately, not when the query first runs.

A catalogue of query methods

The repository below declares one method for each common kind of query. The entity is the same Book as before - id, title, author, publishedYear and price - and a runner fills the database with eight books and runs every method -
Book.java - the entity, as in the previous tutorial
//Book.java
package com.javabrahman.data.derived;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import java.math.BigDecimal;

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

  protected Book() {
  }

  public Book(String title, String author, int publishedYear, BigDecimal price) {
    this.title = title;
    this.author = author;
    this.publishedYear = publishedYear;
    this.price = price;
  }

  public Long getId() { return id; }
  public String getTitle() { return title; }
  public String getAuthor() { return author; }
  public int getPublishedYear() { return publishedYear; }
  public BigDecimal getPrice() { return price; }

  @Override
  public String toString() {
    return title + " (" + author + ", " + publishedYear + ", $" + price + ")";
  }
}
BookRepository.java - derived query methods
//BookRepository.java
package com.javabrahman.data.derived;

import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;

public interface BookRepository extends JpaRepository<Book, Long> {

  // equality
  List<Book> findByAuthor(String author);
  Optional<Book> findByTitle(String title);
  List<Book> findByAuthorAndPublishedYear(String author, int publishedYear);
  List<Book> findByAuthorOrPublishedYear(String author, int publishedYear);

  // case, text matching, collections
  List<Book> findByAuthorIgnoreCase(String author);
  List<Book> findByTitleContaining(String text);
  List<Book> findByTitleStartingWithIgnoreCase(String prefix);
  List<Book> findByAuthorIn(Collection<String> authors);

  // comparisons and ranges
  List<Book> findByPublishedYearBetween(int from, int to);
  List<Book> findByPriceLessThan(BigDecimal price);
  List<Book> findByPublishedYearGreaterThanEqual(int year);
  List<Book> findByAuthorNot(String author);

  // sorting and limiting
  List<Book> findByPublishedYearGreaterThanEqualOrderByPriceDesc(int year);
  List<Book> findByAuthorOrderByPublishedYearAsc(String author);
  List<Book> findByPublishedYearGreaterThan(int year, Sort sort);
  Book findFirstByOrderByPriceAsc();
  List<Book> findTop3ByOrderByPublishedYearDesc();
  List<Book> findByPublishedYearGreaterThan(int year, Sort sort, Limit limit);

  // distinct, count, exists, delete
  List<String> findDistinctAuthorBy();   // looks like "the distinct authors" - see the output and explanation
  long countByAuthor(String author);
  boolean existsByTitle(String title);

  @Transactional
  long deleteByPublishedYearLessThan(int year);
}
DerivedQueriesApp.java - runs every method
//DerivedQueriesApp.java
package com.javabrahman.data.derived;

import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Sort;

@SpringBootApplication
public class DerivedQueriesApp {

  @Bean
  CommandLineRunner demo(BookRepository r) {
    return args -> {
      r.saveAll(List.of(
          new Book("Effective Java", "Joshua Bloch", 2018, new BigDecimal("45.99")),
          new Book("Java Puzzlers", "Joshua Bloch", 2005, new BigDecimal("29.99")),
          new Book("Java Concurrency in Practice", "Brian Goetz", 2006, new BigDecimal("49.99")),
          new Book("Clean Code", "Robert C. Martin", 2008, new BigDecimal("37.49")),
          new Book("Clean Architecture", "Robert C. Martin", 2017, new BigDecimal("34.99")),
          new Book("Refactoring", "Martin Fowler", 2018, new BigDecimal("47.99")),
          new Book("Spring in Action", "Craig Walls", 2022, new BigDecimal("49.99")),
          new Book("Domain-Driven Design", "Eric Evans", 2003, new BigDecimal("59.99"))));

      show("findByAuthor(\"Joshua Bloch\")", r.findByAuthor("Joshua Bloch"));
      System.out.println("findByTitle(\"Clean Code\"): " + r.findByTitle("Clean Code").map(Book::getAuthor).orElse("-"));
      System.out.println("findByTitle(\"Unknown\"): " + r.findByTitle("Unknown"));
      show("findByAuthorAndPublishedYear(\"Joshua Bloch\", 2018)", r.findByAuthorAndPublishedYear("Joshua Bloch", 2018));
      show("findByAuthorOrPublishedYear(\"Brian Goetz\", 2018)", r.findByAuthorOrPublishedYear("Brian Goetz", 2018));
      System.out.println();
      show("findByAuthorIgnoreCase(\"joshua BLOCH\")", r.findByAuthorIgnoreCase("joshua BLOCH"));
      show("findByTitleContaining(\"Clean\")", r.findByTitleContaining("Clean"));
      show("findByTitleStartingWithIgnoreCase(\"java\")", r.findByTitleStartingWithIgnoreCase("java"));
      show("findByAuthorIn([Brian Goetz, Eric Evans])", r.findByAuthorIn(List.of("Brian Goetz", "Eric Evans")));
      System.out.println();
      show("findByPublishedYearBetween(2005, 2008)", r.findByPublishedYearBetween(2005, 2008));
      show("findByPriceLessThan(35)", r.findByPriceLessThan(new BigDecimal("35")));
      show("findByPublishedYearGreaterThanEqual(2018)", r.findByPublishedYearGreaterThanEqual(2018));
      show("findByAuthorNot(\"Robert C. Martin\") - first 3 shown", r.findByAuthorNot("Robert C. Martin").subList(0, 3));
      System.out.println();
      show("findByPublishedYearGreaterThanEqualOrderByPriceDesc(2017)", r.findByPublishedYearGreaterThanEqualOrderByPriceDesc(2017));
      show("findByAuthorOrderByPublishedYearAsc(\"Robert C. Martin\")", r.findByAuthorOrderByPublishedYearAsc("Robert C. Martin"));
      show("findByPublishedYearGreaterThan(2005, Sort.by(\"title\"))", r.findByPublishedYearGreaterThan(2005, Sort.by("title")));
      System.out.println("findFirstByOrderByPriceAsc(): " + r.findFirstByOrderByPriceAsc());
      show("findTop3ByOrderByPublishedYearDesc()", r.findTop3ByOrderByPublishedYearDesc());
      show("findByPublishedYearGreaterThan(2005, Sort.by(\"price\").descending(), Limit.of(2))",
          r.findByPublishedYearGreaterThan(2005, Sort.by("price").descending(), Limit.of(2)));
      System.out.println();
      try {
        System.out.println("findDistinctAuthorBy(): " + r.findDistinctAuthorBy());
      } catch (Exception e) {
        String message = e.getMessage();
        System.out.println("findDistinctAuthorBy() failed: " + e.getClass().getSimpleName() + " - " + message.substring(0, message.indexOf(" for value")));
      }
      System.out.println("countByAuthor(\"Joshua Bloch\"): " + r.countByAuthor("Joshua Bloch"));
      System.out.println("existsByTitle(\"Refactoring\"): " + r.existsByTitle("Refactoring"));
      System.out.println("deleteByPublishedYearLessThan(2006) deleted " + r.deleteByPublishedYearLessThan(2006) + " books, "
          + r.count() + " left");
    };
  }

  private static void show(String call, Collection<Book> books) {
    System.out.println(call + ":");
    books.forEach(b -> System.out.println("    " + b));
  }

  public static void main(String[] args) {
    SpringApplication.run(DerivedQueriesApp.class, args);
  }
}
 OUTPUT of the above code
findByAuthor("Joshua Bloch"):
    Effective Java (Joshua Bloch, 2018, $45.99)
    Java Puzzlers (Joshua Bloch, 2005, $29.99)
findByTitle("Clean Code"): Robert C. Martin
findByTitle("Unknown"): Optional.empty
findByAuthorAndPublishedYear("Joshua Bloch", 2018):
    Effective Java (Joshua Bloch, 2018, $45.99)
findByAuthorOrPublishedYear("Brian Goetz", 2018):
    Effective Java (Joshua Bloch, 2018, $45.99)
    Java Concurrency in Practice (Brian Goetz, 2006, $49.99)
    Refactoring (Martin Fowler, 2018, $47.99)

findByAuthorIgnoreCase("joshua BLOCH"):
    Effective Java (Joshua Bloch, 2018, $45.99)
    Java Puzzlers (Joshua Bloch, 2005, $29.99)
findByTitleContaining("Clean"):
    Clean Code (Robert C. Martin, 2008, $37.49)
    Clean Architecture (Robert C. Martin, 2017, $34.99)
findByTitleStartingWithIgnoreCase("java"):
    Java Puzzlers (Joshua Bloch, 2005, $29.99)
    Java Concurrency in Practice (Brian Goetz, 2006, $49.99)
findByAuthorIn([Brian Goetz, Eric Evans]):
    Java Concurrency in Practice (Brian Goetz, 2006, $49.99)
    Domain-Driven Design (Eric Evans, 2003, $59.99)

findByPublishedYearBetween(2005, 2008):
    Java Puzzlers (Joshua Bloch, 2005, $29.99)
    Java Concurrency in Practice (Brian Goetz, 2006, $49.99)
    Clean Code (Robert C. Martin, 2008, $37.49)
findByPriceLessThan(35):
    Java Puzzlers (Joshua Bloch, 2005, $29.99)
    Clean Architecture (Robert C. Martin, 2017, $34.99)
findByPublishedYearGreaterThanEqual(2018):
    Effective Java (Joshua Bloch, 2018, $45.99)
    Refactoring (Martin Fowler, 2018, $47.99)
    Spring in Action (Craig Walls, 2022, $49.99)
findByAuthorNot("Robert C. Martin") - first 3 shown:
    Effective Java (Joshua Bloch, 2018, $45.99)
    Java Puzzlers (Joshua Bloch, 2005, $29.99)
    Java Concurrency in Practice (Brian Goetz, 2006, $49.99)

findByPublishedYearGreaterThanEqualOrderByPriceDesc(2017):
    Spring in Action (Craig Walls, 2022, $49.99)
    Refactoring (Martin Fowler, 2018, $47.99)
    Effective Java (Joshua Bloch, 2018, $45.99)
    Clean Architecture (Robert C. Martin, 2017, $34.99)
findByAuthorOrderByPublishedYearAsc("Robert C. Martin"):
    Clean Code (Robert C. Martin, 2008, $37.49)
    Clean Architecture (Robert C. Martin, 2017, $34.99)
findByPublishedYearGreaterThan(2005, Sort.by("title")):
    Clean Architecture (Robert C. Martin, 2017, $34.99)
    Clean Code (Robert C. Martin, 2008, $37.49)
    Effective Java (Joshua Bloch, 2018, $45.99)
    Java Concurrency in Practice (Brian Goetz, 2006, $49.99)
    Refactoring (Martin Fowler, 2018, $47.99)
    Spring in Action (Craig Walls, 2022, $49.99)
findFirstByOrderByPriceAsc(): Java Puzzlers (Joshua Bloch, 2005, $29.99)
findTop3ByOrderByPublishedYearDesc():
    Spring in Action (Craig Walls, 2022, $49.99)
    Refactoring (Martin Fowler, 2018, $47.99)
    Effective Java (Joshua Bloch, 2018, $45.99)
findByPublishedYearGreaterThan(2005, Sort.by("price").descending(), Limit.of(2)):
    Spring in Action (Craig Walls, 2022, $49.99)
    Java Concurrency in Practice (Brian Goetz, 2006, $49.99)

findDistinctAuthorBy() failed: ConversionFailedException - Failed to convert from type [java.util.ArrayList<?>] to type [java.util.List<java.lang.String>]
countByAuthor("Joshua Bloch"): 2
existsByTitle("Refactoring"): true
deleteByPublishedYearLessThan(2006) deleted 2 books, 6 left

Explanation of the output

  • Equality: findByAuthor compares the author property with the parameter. A method returning Optional gives an empty one when nothing matches, and fails with an exception if more than one row matches - use it only where the property is unique.
  • And and Or combine properties in the order the parameters are given. There is no way to group them with brackets; that is one reason long names stop working.
  • IgnoreCase compares in a case-insensitive way; Containing, StartingWith and EndingWith become like comparisons, and Spring Data adds the % wildcards and escapes any in your text. In takes a collection.
  • Comparisons: Between is inclusive on both ends - 2005 and 2008 both matched; LessThan, GreaterThan, their OrEqual variants, Not, IsNull and IsNotNull work as named.
  • Sorting: OrderBy...Asc/Desc fixes the order in the name. A Sort parameter lets the caller choose the order instead, as in Sort.by("title").
  • Limiting: First and Top3 in the name fix the number of results. A Limit parameter - new in Spring Data 3.2 - lets the caller choose it, here the two most expensive books.
  • findDistinctAuthorBy() looks as if it returns the distinct authors, but it fails. A derived query always selects entities: Distinct applies to whole Book rows, and the name has no way to say "only the author column". Spring Data then cannot convert a list of Book objects to a List<String> and throws a ConversionFailedException when the method is called - the one mistake in this repository that startup validation does not catch. Getting only some columns is the subject of Spring Data JPA Projections and DTOsClick to read tutorial: Spring Data JPA Projections and DTOs.
  • countBy returns a number, existsBy a boolean - both are answered by the database without loading any entity.
  • deleteBy loads the matching entities and deletes them one by one - which is why it needs a transaction (@Transactional on the method) and returns the number deleted. For large deletes, a bulk @Modifying @Query is faster; see the next tutorial.

The SQL behind the method names

The statements for some of the methods, with spring.jpa.show-sql=true - note how names become where clauses, parameters and ordering -  OUTPUT - selected SQL statements (the lines for ten of the methods)
Hibernate: select b1_0.id,b1_0.author,b1_0.price,b1_0.published_year,b1_0.title from book b1_0 where b1_0.author=? and b1_0.published_year=?
Hibernate: select b1_0.id,b1_0.author,b1_0.price,b1_0.published_year,b1_0.title from book b1_0 where upper(b1_0.author)=upper(?)
Hibernate: select b1_0.id,b1_0.author,b1_0.price,b1_0.published_year,b1_0.title from book b1_0 where b1_0.title like ? escape '\'
Hibernate: select b1_0.id,b1_0.author,b1_0.price,b1_0.published_year,b1_0.title from book b1_0 where b1_0.published_year between ? and ?
Hibernate: select b1_0.id,b1_0.author,b1_0.price,b1_0.published_year,b1_0.title from book b1_0 where b1_0.published_year>=? order by b1_0.price desc
Hibernate: select b1_0.id,b1_0.author,b1_0.price,b1_0.published_year,b1_0.title from book b1_0 where b1_0.published_year>? order by b1_0.price desc fetch first ? rows only
Hibernate: select distinct b1_0.id,b1_0.author,b1_0.price,b1_0.published_year,b1_0.title from book b1_0
Hibernate: select count(b1_0.id) from book b1_0 where b1_0.author=?
Hibernate: select b1_0.id from book b1_0 where b1_0.title=? fetch first ? rows only
Hibernate: delete from book where id=?
Hibernate: delete from book where id=?

Explanation of the SQL

  • findByAuthorAndPublishedYear became where author=? and published_year=?: property names turned into the snake-case column names, values bound as parameters. Between became SQL between ? and ?.
  • IgnoreCase became upper(author)=upper(?) - both sides are converted, so a database index on author cannot be used for it. StartingWithIgnoreCase and Containing became like ? escape '\' with the wildcards added to the parameter.
  • OrderBy...Desc became order by ... desc, and Top3, First and Limit became fetch first ? rows only, so the database stops after the requested rows.
  • countBy became select count(id). existsBy became a query for one id with fetch first ? rows only: it stops at the first match.
  • findDistinctAuthorBy became select distinct over all the columns of the entity, which confirms why it cannot return authors.
  • deleteByPublishedYearLessThan ran one select followed by a separate delete from book where id=? for each row - two books, two deletes.

A wrong method name fails at startup

Spring Data checks every method name against the entity before the application finishes starting. A repository with findByAuthors, when the entity's property is called author, fails with a precise message -
A repository with a method name that matches no property
//BrokenRepository.java
package com.javabrahman.data.brokenrepo;

import com.javabrahman.data.derived.Book;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BrokenRepository extends JpaRepository<Book, Long> {
  List<Book> findByAuthors(String author);   // the entity has "author", not "authors"
}
BrokenApp.java - starts with the broken repository and prints the cause
//BrokenApp.java
package com.javabrahman.data.brokenrepo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.persistence.autoconfigure.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication
@EntityScan("com.javabrahman.data.derived")
@EnableJpaRepositories(basePackageClasses = BrokenRepository.class)
public class BrokenApp {
  public static void main(String[] args) {
    try (var context = SpringApplication.run(BrokenApp.class, args)) {
      System.out.println("Started");
    } catch (Exception e) {
      Throwable cause = e;
      while (cause.getCause() != null && !cause.getClass().getSimpleName().equals("PropertyReferenceException")) {
        cause = cause.getCause();
      }
      System.out.println(cause.getClass().getSimpleName() + ": " + cause.getMessage());
    }
  }
}
 OUTPUT of the above code
PropertyReferenceException: No property 'authors' found for type 'Book'; Did you mean 'author'
The message names the missing property, the entity, and even suggests the closest existing property (Did you mean 'author'). Compare this with a mistake in a SQL string, which would only show up when the query runs.

Supported keywords

The keywords most often used in the predicate -
  • Equality and null: Is/Equals (the default), Not, IsNull, IsNotNull.
  • Numbers and dates: LessThan, LessThanEqual, GreaterThan, GreaterThanEqual, Between, Before, After.
  • Text: Like, NotLike, StartingWith, EndingWith, Containing, NotContaining, IgnoreCase, AllIgnoreCase.
  • Collections: In, NotIn.
  • Booleans: True, False (no parameter needed: findByActiveTrue()).
  • Properties of related entities use a path: findByPublisherName(String name) joins to the publisher and filters on its name (covered with relationships in JPA Entity RelationshipsClick to read tutorial: JPA Entity Relationships: OneToOne, OneToMany, ManyToMany).

When a derived query is the wrong tool

  • The name becomes unreadable: findByAuthorAndPublishedYearGreaterThanEqualAndPriceLessThanOrderByTitleAsc says everything and is still hard to read. As a rule of thumb, more than two or three conditions call for @Query, with a short, descriptive method name of your own.
  • Complex conditions: grouping with brackets, subqueries, aggregates (sum, avg, group by) and joins that need explicit control cannot be expressed in a name.
  • Only some columns, as the findDistinctAuthorBy example showed, need projections.
  • Dynamic filters, where the set of conditions is decided at runtime, are better served by Spring Data's Specification or Querydsl support.
Derived queries remain the best choice for the simple, frequent lookups - by a key, by one or two properties - which are most of the queries in most applications.

Summary

In this tutorial we first saw how Spring Data parses a method name into a subject, a predicate and an ordering, and checks it at startup. We then ran a catalogue of derived queries on eight books: equality with And and Or, case-insensitive and text matching, In, ranges and comparisons, sorting with OrderBy and Sort, limiting with Top, First and Limit, and counts, existence checks and deletes. We looked at the SQL generated for some of them, saw the startup error for a name that matches no property, and listed the supported keywords. Lastly we saw when a long method name is the wrong tool. The next tutorial writes queries by hand: @Query with JPQL and native SQLClick to read tutorial: Spring Data JPA @Query: JPQL and Native Queries.