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.
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>
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);
}
}
== 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, notyear, on purpose. Hibernate uses field names as column names, andYEARis a reserved word in SQL: with a field namedyear, the very first run of this tutorial failed when H2 refusedcreate 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`"). BookRepositoryhas 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 fromJpaRepository.save()stores an entity. For a new object, with no id, it performs aninsertand the database's generated id is set on the object - the saved book printed its id.saveAll()saves several at once.findById()returns anOptional: an empty one for the unknown id 99, instead ofnull.existsById(),count()andfindAll()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 callingsave()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: setspring.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 existsandcreate tablefrom the entity:publishedYearbecamepublished_year(Spring Boot's naming strategy turns camel case into snake case), theidcolumn is an identity column, andintbecameinteger not null. At shutdown it dropped the table again. - Each
save()of a new book became oneinsertstatement 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 aselect ... where id=?;findAll()aselectof every column of every row;count()aselect count(*).- Two statements may surprise you.
save()on the existing book first ran aselectbefore theupdate: for an object that already has an id,save()does not know whether the row exists, so Hibernate loads it and then updates it. AnddeleteById()also selected the row before deleting it. (In the update step, the firstselectis our ownfindById(), 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
DataSourcethat uses HikariCP's connection pool; - Hibernate as the JPA provider, with an
EntityManagerFactoryand a JPA transaction manager; - entity scanning for
@Entityclasses 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.
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 anIterable),delete,countandexistsById.ListCrudRepository: the same, withListinstead ofIterableas the result type.PagingAndSortingRepository: addsfindAll(Sort)andfindAll(Pageable).JpaRepository: all of the above, plus JPA-specific methods such asflush()andsaveAndFlush(), anddeleteAllInBatch(). It is the usual choice for JPA, and the one used in this series.
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-sqlprints the SQL;logging.level.org.hibernate.SQL=DEBUGlogs it through the logging system instead, andlogging.level.org.hibernate.orm.jdbc.bind=TRACEadds the parameter values.spring.jpa.hibernate.ddl-auto:none,validate(check that the tables match the entities),update,create,create-drop. Usevalidateornonewith a migration tool in production; neverupdateorcreate.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 tofalse. 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.@Transientexcludes a field from the mapping;@Enumerated(EnumType.STRING)stores an enum by name instead of by number;@Lobstores 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 aBook 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.Spring Data Tutorials
Spring Data JPA Derived Query Methods ExplainedClick to read tutorial: Spring Data JPA Derived Query Methods Explained 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 Derived Query Methods ExplainedClick to read tutorial: Spring Data JPA Derived Query Methods Explained 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