Dev Learning Lab

The labs on this site need JavaScript to run. The same content is written out below.

Learn › JPA › Queries

Spring Data JPA

How does a repository work when there is no implementation class?

Spring Data JPA is a Spring module that makes JPA easier to use, and a repository is an interface that gathers the methods for saving and querying entities. In Spring Data JPA, you declare a repository only as an interface. Spring Data creates the implementation when the application starts, and a method such as findByName is read by its name and turned into a query. This lab shows how a method name becomes a query, and also which SQL is actually sent when a page is requested and when save() is called.

Checked against real execution · 19 scenes were run by starting Spring Data JPA 4.1.1, Hibernate 7.4.5, and H2 with the auto-configuration of Spring Boot 4.1.1, and the startup result and the SQL sent were checked against the results

Concepts in this lab

JPA, Hibernate, Spring Data JPA
JPA (Jakarta Persistence) is the standard ORM specification for Java: a set of rules (interfaces) that defines which features exist and how to use them. Hibernate is a library that actually implements that specification. Spring Data JPA is a Spring module that provides repository interfaces to make JPA easier to use, and it uses JPA (Hibernate) internally.
Query method
A feature of Spring Data repositories that reads the name of a method declared in the repository interface and derives a query from it (a derived query). As in findByName, you write the property name of the entity after find…By, attach a keyword such as GreaterThan or Containing after the property name to set the kind of condition, and join several conditions with And or Or. The name is turned into a query when the application starts, so if you use a property name that the entity does not have, a PropertyReferenceException is thrown and the application does not start. When there are many conditions, the name becomes too long, so write the JPQL directly in @Query then.
Pageable and Page
Pageable is an object that holds a request for "which page, how many rows per page, and in what order", and it is usually created with PageRequest.of(page number, size). Page numbers start from 0. If the return type is Page, Spring Data sends a query that reads the content and usually one more count query that counts the total, and returns the content of the page together with the total number of elements and the total number of pages. If the return type is Slice, it reads one extra row without a count query and tells you only whether a next slice exists. On a table with a lot of data the count query can be slow, so first think about whether you really need the total.
save() and merge
The save() of Spring Data JPA calls em.persist if the entity it receives is new, and em.merge otherwise. By default, whether an entity is new is decided by whether the id is null. If there is a @Version field and its type is an object type such as Long, it is decided by whether that value is null (for a primitive type such as long, the id decides). merge first queries the row with the same id with a SELECT, overwrites the managed object with the values of the object it received, and returns that managed object. So an entity whose id is set directly in code also sends one more SELECT the first time it is saved. To avoid this, let the entity implement the Persistable interface and tell Spring Data directly whether it is new.
Entity
A Java object that corresponds to a row of a database table. It is defined as a class annotated with @Entity and has an identifier (@Id). JPA tracks the state changes of these objects and runs the matching SQL.

Try these

Running with the default settings

  1. Application start: Spring Data JPA finds the interface MemberRepository, which extends JpaRepository. You never wrote an implementation class, but Spring Data creates an implementation object (a proxy) of this interface on your behalf and registers it as a bean.
  2. Reading the method name: The method findByNameAndAgeGreaterThan has no body written by a person. Spring Data splits the name, reads it, and builds a query. The find…By at the beginning means "select the matching rows", and what follows By is the condition.
  3. Name: No keyword is attached, so the condition is "equals". It checks whether the Member entity has a property with this name (name). It does, so the condition in the query is "name = ?". The question mark is where the value passed in the call goes.
  4. And AgeGreaterThan: And means joining with the previous condition by and. The keyword at the end (GreaterThan) means "greater than". It checks whether the Member entity has a property with this name (age). It does, so the condition in the query is "age > ?". The question mark is where the value passed in the call goes.
  5. Application startup complete: Reading the name is finished and the query is ready. This reading and the property check are done in advance, when the application starts, not when the method is called.
  6. Call to memberRepository.findByNameAndAgeGreaterThan("Alice", 20): The implementation object puts the values you passed into the question mark positions of the prepared query and sends one SELECT.
  7. Result: It returns the 1 member that match the condition as a list of Member objects.

Result of this run

Code

@Entity
class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
    String name;
    int age;
    // constructor and getters omitted
}

// Do not write an implementation class. Declare only the interface
interface MemberRepository extends JpaRepository<Member, Long> {
    List<Member> findByNameAndAgeGreaterThan(String name, int age);
}

// service
List<Member> result = memberRepository.findByNameAndAgeGreaterThan("Alice", 20);

Assumptions of this model

All the scenes (6 method names, 8 page requests, and 5 save cases) were run on the PC used to write this lab by actually starting Spring Data JPA 4.1.1, Hibernate 7.4.5, and an H2 database with the auto-configuration of Spring Boot 4.1.1, and whether startup succeeded, the exception name and message, the number and kind of SQL statements sent to the database, the shape of where and order by, the number of rows skipped and requested, and the results received were checked against the screen. It was also confirmed by execution that the implementation of the repository is a proxy and that SimpleJpaRepository is behind it. The SQL on the screen is shortened to be easy to read. The actual SQL has table aliases, and the like condition has an escape clause added to it, and the shape of the statement differs by Hibernate version and database. The two wrong method names were checked by starting a separate repository that has only that method. Other keywords mentioned in the result summaries, such as Or, follow the keyword table in the Spring Data JPA documentation and were not run. The decision of whether an entity is new in save follows the "Entity State-detection Strategies" section of the Spring Data JPA documentation, and for an entity with a @Version field, the decision is made by whether that field is null instead of the id. The SELECT being sent twice and the exception being thrown in the id 999 example are results seen with Hibernate 7.4.5, and they differ by version. It was also confirmed by execution that when an entity whose id is set directly implements Persistable and tells Spring Data that it is new, only one INSERT is sent without a SELECT (the way of adding a @Version field was not run). save was called with no transaction outside it, so the UPDATE and INSERT are sent when the transaction of save itself ends. If there is a @Transactional outside, they are sent when that transaction commits.

Previous lab: Bulk update and delete operations · Next lab: Dynamic queries and Querydsl

Concepts in this lab

What this lab shows

Settings

Code


      
    

Diagram

Run comparison

Try these