Spring Data JPA Introduction: Entities and Repositories

Introduction

This tutorial introduces Spring Data JPA, the part of Spring that lets you store Java objects in a relational database without writing SQL or any data access code. We will start with the three names that always appear together - JPA, Hibernate and Spring Data JPA - and what each one does. We will then add the dependencies, define an entity that maps a Java class to a table, and declare a repository as an interface with no implementation. Next we will run the application and watch every CRUD operation - create, read, update and delete - together with the SQL that Hibernate generates for it. Lastly, we will look at what Spring Boot configures for you, the repository interfaces to choose from, and the properties you will use most.

(This tutorial starts module 4 of the Spring series. It assumes the basics from Your First Spring Boot Application with Spring InitializrClick to read tutorial: Your First Spring Boot Application with Spring Initializr and Spring Boot Starters ExplainedClick to read tutorial: Spring Boot Starters Explained with Examples.)

JPA, Hibernate and Spring Data JPA

Three layers work together, and it helps to keep them apart -
  • JPA (Jakarta Persistence API) is a specification: a set of interfaces and annotations - @Entity, @Id, EntityManager - that describe how to map Java objects to database tables. It contains no code that talks to a database.
  • Hibernate is the implementation of JPA that Spring Boot uses. It turns the annotations into table definitions, translates operations on objects into SQL, and converts query results back into objects. This mapping is called object-relational mapping (ORM).
  • Spring Data JPA sits on top of both. You declare a repository interface, and Spring Data generates the implementation, so even the code that calls Hibernate disappears.
A table row becomes an object, a column a field, and a table a class. You work with Book objects; the library works with book rows.

Dependencies

Two dependencies are enough for an experiment: the Spring Data JPA starter, which brings Hibernate, Spring Data JPA, a connection pool (HikariCP) and transaction support, and a database. This series uses H2, a small database written in Java that can run in memory inside the application, so nothing has to be installed and every run starts with an empty database. For a real project, you would add the driver of your database - PostgreSQL, MySQL - instead -
pom.xml - the dependencies for Spring Data JPA with an in-memory H2 database
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <scope>runtime</scope>
</dependency>
On Spring Initializr, these are the Spring Data JPA and H2 Database entries. The examples in this module run on Spring Boot 4.1.2 with Spring Data JPA 4.1.2 and Hibernate 7.4.

The entity and the repository

An entity is a Java class annotated with @Entity. The class becomes a table, and its fields become columns. The repository for it is an interface extending JpaRepository<Book, Long> - the entity type and the type of its id. A small runner then exercises the repository -
Book.java - the entity
//Book.java
package com.javabrahman.data.intro;

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

@Entity   // mapped to a table named "book"
public class Book {

  @Id   // the primary key
  @GeneratedValue(strategy = GenerationType.IDENTITY)   // the database assigns the id
  private Long id;

  private String title;
  private String author;
  private int publishedYear;   // not "year": YEAR is a reserved word in SQL (see the explanation below)
  private BigDecimal price;

  protected Book() {   // JPA needs a no-argument constructor; protected keeps application code from using it
  }

  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; }
  public void setPrice(BigDecimal price) { this.price = price; }

  @Override
  public String toString() {
    return "Book[id=" + id + ", title=" + title + ", author=" + author + ", publishedYear=" + publishedYear + ", price=" + price + "]";
  }
}
BookRepository.java - the repository: an interface, and nothing else
//BookRepository.java
package com.javabrahman.data.intro;

import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepository extends JpaRepository<Book, Long> {
}
IntroApp.java - using the repository
//IntroApp.java
package com.javabrahman.data.intro;

import java.math.BigDecimal;
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;

@SpringBootApplication
public class IntroApp {

  @Bean
  CommandLineRunner demo(BookRepository repository) {
    return args -> {
      System.out.println("== create");
      Book effective = repository.save(new Book("Effective Java", "Joshua Bloch", 2018, new BigDecimal("45.99")));
      repository.save(new Book("Java Concurrency in Practice", "Brian Goetz", 2006, new BigDecimal("49.99")));
      repository.saveAll(List.of(
          new Book("Clean Code", "Robert C. Martin", 2008, new BigDecimal("37.49")),
          new Book("Refactoring", "Martin Fowler", 2018, new BigDecimal("47.99"))));
      System.out.println("saved " + effective + ", count = " + repository.count());

      System.out.println("== read");
      System.out.println("findById(1): " + repository.findById(1L).orElseThrow());
      System.out.println("findById(99): " + repository.findById(99L));
      System.out.println("existsById(2): " + repository.existsById(2L));
      System.out.println("findAll(): ");
      repository.findAll().forEach(book -> System.out.println("  " + book));

      System.out.println("== update");
      Book book = repository.findById(1L).orElseThrow();
      book.setPrice(new BigDecimal("39.99"));
      repository.save(book);   // an existing id, so this is an update
      System.out.println("after the update: " + repository.findById(1L).orElseThrow());

      System.out.println("== delete");
      repository.deleteById(2L);
      System.out.println("count after deleting book 2: " + repository.count());
    };
  }

  public static void main(String[] args) {
    SpringApplication.run(IntroApp.class, args);
  }
}
 OUTPUT of the above code
== create
saved Book[id=1, title=Effective Java, author=Joshua Bloch, publishedYear=2018, price=45.99], count = 4
== read
findById(1): Book[id=1, title=Effective Java, author=Joshua Bloch, publishedYear=2018, price=45.99]
findById(99): Optional.empty
existsById(2): true
findAll():
  Book[id=1, title=Effective Java, author=Joshua Bloch, publishedYear=2018, price=45.99]
  Book[id=2, title=Java Concurrency in Practice, author=Brian Goetz, publishedYear=2006, price=49.99]
  Book[id=3, title=Clean Code, author=Robert C. Martin, publishedYear=2008, price=37.49]
  Book[id=4, title=Refactoring, author=Martin Fowler, publishedYear=2018, price=47.99]
== update
after the update: Book[id=1, title=Effective Java, author=Joshua Bloch, publishedYear=2018, price=39.99]
== delete
count after deleting book 2: 3

Explanation of the code

  • The field is called publishedYear, not year, on purpose. Hibernate uses field names as column names, and YEAR is a reserved word in SQL: with a field named year, the very first run of this tutorial failed when H2 refused create table book (price numeric(38,2), year integer not null, ...) with a syntax error. Avoid reserved words as names, or quote them with @Column(name = "`year`").
  • BookRepository has no methods and no implementation class, yet it works. At startup Spring Data creates a proxy object that implements the interface, and registers it as a bean, so it can be injected like any other. The methods come from JpaRepository.
  • save() stores an entity. For a new object, with no id, it performs an insert and the database's generated id is set on the object - the saved book printed its id. saveAll() saves several at once.
  • findById() returns an Optional: an empty one for the unknown id 99, instead of null. existsById(), count() and findAll() do what their names say.
  • Calling save() again on an object that already has an id is an update: the price of book 1 changed, and the next read shows it. (The next tutorials explain why a change can even be saved without calling save() inside a transaction.)
  • deleteById() removes the row. Each repository call here runs in its own short transaction, which Spring Data provides.

The SQL behind the calls

Hibernate can print every SQL statement it runs: set spring.jpa.show-sql=true. Running the same program again with that setting, the lines that start with Hibernate: are the SQL statements, and the lines starting with == are the program's own step markers -  OUTPUT with spring.jpa.show-sql=true (the SQL statements and the step markers)
Hibernate: drop table if exists book cascade
Hibernate: create table book (price numeric(38,2), published_year integer not null, id bigint generated by default as identity, author varchar(255), title varchar(255), primary key (id))
== create
Hibernate: insert into book (author,price,published_year,title,id) values (?,?,?,?,default)
Hibernate: insert into book (author,price,published_year,title,id) values (?,?,?,?,default)
Hibernate: insert into book (author,price,published_year,title,id) values (?,?,?,?,default)
Hibernate: insert into book (author,price,published_year,title,id) values (?,?,?,?,default)
Hibernate: select count(*) from book b1_0
== read
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.id=?
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.id=?
Hibernate: select count(*) from book b1_0 where b1_0.id=?
Hibernate: select b1_0.id,b1_0.author,b1_0.price,b1_0.published_year,b1_0.title from book b1_0
== update
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.id=?
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.id=?
Hibernate: update book set author=?,price=?,published_year=?,title=? where id=?
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.id=?
== delete
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.id=?
Hibernate: delete from book where id=?
Hibernate: select count(*) from book b1_0
Hibernate: drop table if exists book cascade

Explanation of the output

  • At startup Hibernate ran drop table if exists and create table from the entity: publishedYear became published_year (Spring Boot's naming strategy turns camel case into snake case), the id column is an identity column, and int became integer not null. At shutdown it dropped the table again.
  • Each save() of a new book became one insert statement with ? placeholders (the values are passed separately as parameters, which makes SQL injection through the data impossible), with the id left to the database.
  • findById() became a select ... where id=?; findAll() a select of every column of every row; count() a select count(*).
  • Two statements may surprise you. save() on the existing book first ran a select before the update: for an object that already has an id, save() does not know whether the row exists, so Hibernate loads it and then updates it. And deleteById() also selected the row before deleting it. (In the update step, the first select is our own findById(), and the last one is the read after the update.)
  • Reading the SQL of your own application is the most important habit in working with JPA: it shows exactly what the framework does on your behalf, and, as the N+1 tutorial shows, where it does too much.

What Spring Boot configured

The application has no database configuration at all, yet it worked. Spring Boot's auto-configuration saw H2 on the classpath and set up -
  • an embedded in-memory database with a generated name, and a DataSource that uses HikariCP's connection pool;
  • Hibernate as the JPA provider, with an EntityManagerFactory and a JPA transaction manager;
  • entity scanning for @Entity classes in the application's package and below;
  • repository scanning: Spring Data finds the repository interfaces and creates their proxies;
  • the schema: with an embedded database, Hibernate creates the tables from the entities at startup (ddl-auto=create-drop) and drops them at shutdown.
For a real database, you set the connection in application.properties, and take over the schema from Hibernate, as Database Migrations with Flyway and LiquibaseClick to read tutorial: Database Migrations with Flyway and Liquibase in Spring Boot explains -
application.properties - a PostgreSQL database, with Hibernate only checking the schema
spring.datasource.url=jdbc:postgresql://localhost:5432/shop
spring.datasource.username=shop
spring.datasource.password=${DB_PASSWORD}
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false

Which repository interface?

Spring Data has a family of repository interfaces, each adding to the one before -
  • Repository<T, ID>: a marker with no methods. Declare only the methods you want, copying signatures from the others.
  • CrudRepository: save, findById, findAll (returning an Iterable), delete, count and existsById.
  • ListCrudRepository: the same, with List instead of Iterable as the result type.
  • PagingAndSortingRepository: adds findAll(Sort) and findAll(Pageable).
  • JpaRepository: all of the above, plus JPA-specific methods such as flush() and saveAndFlush(), and deleteAllInBatch(). It is the usual choice for JPA, and the one used in this series.
Exposing the full JpaRepository gives every caller methods such as deleteAll(). In a larger application, you may prefer to extend the plain Repository and declare only what the application should be able to do.

Useful properties and annotations

  • spring.jpa.show-sql prints the SQL; logging.level.org.hibernate.SQL=DEBUG logs it through the logging system instead, and logging.level.org.hibernate.orm.jdbc.bind=TRACE adds the parameter values.
  • spring.jpa.hibernate.ddl-auto: none, validate (check that the tables match the entities), update, create, create-drop. Use validate or none with a migration tool in production; never update or create.
  • spring.jpa.open-in-view: Spring Boot keeps a JPA session open for the whole web request by default, which allows lazy loading in controllers but hides queries there; many teams set it to false. The lazy-loading tutorial returns to this.
  • @Table(name = "books") and @Column(name = "published_year", nullable = false, length = 100) override the default table and column names and add constraints.
  • @Transient excludes a field from the mapping; @Enumerated(EnumType.STRING) stores an enum by name instead of by number; @Lob stores large text or binary data.

Summary

In this tutorial we first separated JPA, Hibernate and Spring Data JPA: a specification, its implementation, and the layer that generates repository implementations. We added the starter and an in-memory H2 database, mapped a Book class with @Entity, @Id and @GeneratedValue, and declared a BookRepository interface. We then ran create, read, update and delete operations and looked at the insert and select statements Hibernate generated. Next we listed what Spring Boot configures automatically, compared the repository interfaces, and collected the properties and annotations used most. The next tutorial shows how to query by other fields without writing any SQL: derived query methodsClick to read tutorial: Spring Data JPA Derived Query Methods Explained.