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 asfindByAuthorAndPublishedYear 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...Byanddelete...By(orremove...By); words between the verb andByare ignored, sofindBooksByAuthorworks, andDistinct,FirstandTop3are recognised there; - a predicate after
By: property names of the entity, each optionally followed by a keyword such asBetween,LessThanorContaining, and joined withAndandOr; - 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, aList, alongfor counts, abooleanfor existence.
A catalogue of query methods
The repository below declares one method for each common kind of query. The entity is the sameBook 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);
}
}
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:
findByAuthorcompares theauthorproperty with the parameter. A method returningOptionalgives 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. AndandOrcombine 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.IgnoreCasecompares in a case-insensitive way;Containing,StartingWithandEndingWithbecomelikecomparisons, and Spring Data adds the%wildcards and escapes any in your text.Intakes a collection.- Comparisons:
Betweenis inclusive on both ends - 2005 and 2008 both matched;LessThan,GreaterThan, theirOrEqualvariants,Not,IsNullandIsNotNullwork as named. - Sorting:
OrderBy...Asc/Descfixes the order in the name. ASortparameter lets the caller choose the order instead, as inSort.by("title"). - Limiting:
FirstandTop3in the name fix the number of results. ALimitparameter - 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:Distinctapplies to wholeBookrows, and the name has no way to say "only the author column". Spring Data then cannot convert a list ofBookobjects to aList<String>and throws aConversionFailedExceptionwhen 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.countByreturns a number,existsBya boolean - both are answered by the database without loading any entity.deleteByloads the matching entities and deletes them one by one - which is why it needs a transaction (@Transactionalon the method) and returns the number deleted. For large deletes, a bulk@Modifying @Queryis faster; see the next tutorial.
The SQL behind the method names
The statements for some of the methods, withspring.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
findByAuthorAndPublishedYearbecamewhere author=? and published_year=?: property names turned into the snake-case column names, values bound as parameters.Betweenbecame SQLbetween ? and ?.IgnoreCasebecameupper(author)=upper(?)- both sides are converted, so a database index onauthorcannot be used for it.StartingWithIgnoreCaseandContainingbecamelike ? escape '\'with the wildcards added to the parameter.OrderBy...Descbecameorder by ... desc, andTop3,FirstandLimitbecamefetch first ? rows only, so the database stops after the requested rows.countBybecameselect count(id).existsBybecame a query for one id withfetch first ? rows only: it stops at the first match.findDistinctAuthorBybecameselect distinctover all the columns of the entity, which confirms why it cannot return authors.deleteByPublishedYearLessThanran oneselectfollowed by a separatedelete 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 withfindByAuthors, 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());
}
}
}
PropertyReferenceException: No property 'authors' found for type 'Book'; Did you mean 'author'
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 thepublisherand filters on itsname(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:
findByAuthorAndPublishedYearGreaterThanEqualAndPriceLessThanOrderByTitleAscsays 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
findDistinctAuthorByexample showed, need projections. - Dynamic filters, where the set of conditions is decided at runtime, are better served by Spring Data's
Specificationor Querydsl support.
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 withAnd 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.Spring Data Tutorials
Spring Data JPA Introduction: Entities and RepositoriesClick to read tutorial: Spring Data JPA Introduction: Entities and Repositories Spring Data JPA @Query: JPQL and Native QueriesClick to read tutorial: Spring Data JPA @Query: JPQL and Native Queries JPA Entity Relationships: OneToOne, OneToMany, ManyToManyClick to read tutorial: JPA Entity Relationships: OneToOne, OneToMany, ManyToMany JPA Lazy Loading and the N+1 Problem in Spring BootClick to read tutorial: JPA Lazy Loading and the N+1 Problem in Spring Boot Spring @Transactional: Propagation and IsolationClick to read tutorial: Spring @Transactional: Propagation and Isolation Spring Data JPA Projections and DTOsClick to read tutorial: Spring Data JPA Projections and DTOs Spring Data JPA Auditing: @CreatedDate and @CreatedByClick to read tutorial: Spring Data JPA Auditing: @CreatedDate and @CreatedBy
Spring Data JPA Introduction: Entities and RepositoriesClick to read tutorial: Spring Data JPA Introduction: Entities and Repositories Spring Data JPA @Query: JPQL and Native QueriesClick to read tutorial: Spring Data JPA @Query: JPQL and Native Queries JPA Entity Relationships: OneToOne, OneToMany, ManyToManyClick to read tutorial: JPA Entity Relationships: OneToOne, OneToMany, ManyToMany JPA Lazy Loading and the N+1 Problem in Spring BootClick to read tutorial: JPA Lazy Loading and the N+1 Problem in Spring Boot Spring @Transactional: Propagation and IsolationClick to read tutorial: Spring @Transactional: Propagation and Isolation Spring Data JPA Projections and DTOsClick to read tutorial: Spring Data JPA Projections and DTOs Spring Data JPA Auditing: @CreatedDate and @CreatedByClick to read tutorial: Spring Data JPA Auditing: @CreatedDate and @CreatedBy