Skip to content
CampusEduX

Database · Lesson 56 of 95

JpaRepository

Learn how JpaRepository works in Spring Boot 4: save, find, update, delete, sort and page with an empty interface, using a runnable tiffin meals app.

8 min read

Imagine a tiffin service that delivers lunch boxes to offices. The owner keeps a register of meals. Every day the staff do the same small jobs: add a new meal, look one up, change a price, remove a meal that is no longer cooked, and count what is left. If each cook wrote these steps again and again in their own style, the register would be a mess.

A JpaRepository is a ready-made helper that does these everyday jobs for you. You declare an empty interface, and Spring writes the working class. In this topic you will learn what a JpaRepository gives you, how it works, and how to use it for saving, finding, updating, deleting, sorting and paging.

What is JpaRepository?

JpaRepository is an interface from Spring Data JPA. When your own interface extends it, Spring builds an implementation at startup, with methods for the common database jobs on one entity. You write no method bodies.

You give it two types: the entity class and the type of its primary key.

java
public interface MealRepository extends JpaRepository<Meal, Long> { }

That is the whole repository. It inherits its methods from a chain of interfaces, and each adds something:

  • CrudRepository gives save, findById, existsById, count, deleteById and more.
  • ListCrudRepository and ListPagingAndSortingRepository return lists instead of plain iterables, and add sorting and paging.
  • JpaRepository itself adds JPA extras such as flush and saveAndFlush.

Why is it used?

Without it, every entity needs a class full of code that opens an entity manager, runs a query and handles errors. Most of that code is the same for every table. A repository removes it, so you write no boilerplate and make fewer mistakes.

It also gives your code a clear home for database access. Your service classes talk to the repository, and the repository talks to Hibernate. When you need a special search, you add one method to the interface, as you will see in the next topics, and you still write no class body.

How it works

Follow what happens when you call save() on your repository.

text
mealRepository.save(meal) | v Spring proxy (built at startup) | v SimpleJpaRepository does the work | v Hibernate writes the SQL | v Database stores the row

At startup, Spring sees your empty interface and builds a proxy for it. The proxy passes each call to SimpleJpaRepository, a class inside Spring Data JPA that already knows how to do the common jobs with Hibernate. Each call is short, and a transaction is opened for you around it.

The methods you use most fall into a few groups.

text
Create/Update save, saveAll Read one findById, existsById Read many findAll, findAllById Sort and page findAll(Sort), findAll(Pageable) Delete deleteById, delete Count count

Notice that save is used both for a new object and for a changed one. If the object has no id yet, Spring Data inserts it. If the id exists, it updates the row.

Real-Life Example

Think about a bank counter. You do not tell the clerk how the vault works. You say "deposit this", "show my balance", "close this account". The counter offers a small, fixed set of services, and behind it a trained team does the real work. A repository is that counter for your table. You ask save, findById or deleteById, and the team behind it, meaning Spring Data and Hibernate, handles the vault.

Code Example

Let's build the meal register for Dabba Express. The runner adds meals, finds one, updates a price, sorts, pages, and deletes.

text
dabba/ ├─ pom.xml └─ src/main/ ├─ java/com/dabbaexpress/meals/ │ ├─ MealsApplication.java │ ├─ Meal.java │ ├─ MealRepository.java │ └─ MealRunner.java └─ resources/ └─ application.properties

File: pom.xml

xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.dabbaexpress</groupId> <artifactId>meals</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <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> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

File: MealsApplication.java in package com.dabbaexpress.meals

java
package com.dabbaexpress.meals; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MealsApplication { public static void main(String[] args) { SpringApplication.run(MealsApplication.class, args); } }

File: Meal.java in package com.dabbaexpress.meals

java
package com.dabbaexpress.meals; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @Entity public class Meal { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private int price; protected Meal() { } public Meal(String name, int price) { this.name = name; this.price = price; } public String getName() { return name; } public void setPrice(int price) { this.price = price; } @Override public String toString() { return id + " " + name + " Rs" + price; } }

File: MealRepository.java in package com.dabbaexpress.meals

java
package com.dabbaexpress.meals; import org.springframework.data.jpa.repository.JpaRepository; public interface MealRepository extends JpaRepository<Meal, Long> { }

File: MealRunner.java in package com.dabbaexpress.meals

java
package com.dabbaexpress.meals; import java.util.List; import org.springframework.boot.CommandLineRunner; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; @Component public class MealRunner implements CommandLineRunner { private final MealRepository repository; public MealRunner(MealRepository repository) { this.repository = repository; } @Override public void run(String... args) { System.out.println("Saved: " + repository.save(new Meal("Dal Rice", 80))); repository.saveAll(List.of( new Meal("Chicken Thali", 150), new Meal("Paneer Thali", 120), new Meal("Veg Pulao", 90))); System.out.println("Count: " + repository.count()); System.out.println("Found: " + repository.findById(2L).orElseThrow()); System.out.println("Exists 9? " + repository.existsById(9L)); Meal paneer = repository.findById(3L).orElseThrow(); paneer.setPrice(130); System.out.println("Updated: " + repository.save(paneer)); System.out.println("Cheapest first:"); repository.findAll(Sort.by("price")) .forEach(m -> System.out.println("- " + m.getName())); Page<Meal> page = repository.findAll(PageRequest.of(0, 2, Sort.by("price"))); System.out.println("Page 1 of " + page.getTotalPages() + ", items: " + page.getContent().size()); repository.deleteById(2L); System.out.println("After delete: " + repository.count()); } }

File: application.properties in src/main/resources

properties
spring.application.name=meals

Run it with mvn spring-boot:run.

Output:

text
Saved: 1 Dal Rice Rs80 Count: 4 Found: 2 Chicken Thali Rs150 Exists 9? false Updated: 3 Paneer Thali Rs130 Cheapest first: - Dal Rice - Veg Pulao - Paneer Thali - Chicken Thali Page 1 of 2, items: 2 After delete: 3

Code Explained

  • MealRepository has no methods and no class body, yet save, findById and the others work. Spring Data creates the class for you.
  • saveAll stores a list of new meals in one call.
  • findById returns an Optional, because the row may not exist. orElseThrow() gives the meal or throws an error.
  • existsById(9L) answers false, because no meal has that id.
  • Changing the price and calling save updates the row. The id is set, so Spring Data does an update and not an insert.
  • Sort.by("price") sorts by the price field of the entity. Use the Java field name, not the column name.
  • PageRequest.of(0, 2, ...) asks for the first page (pages start at zero) with two items. The Page object also knows the total number of pages.
  • deleteById removes the row, and count shows one fewer.

Main Methods at a Glance

MethodWhat it doesReturns
save(entity)Inserts or updatesThe saved entity
findById(id)Loads one rowOptional<T>
findAll()Loads all rowsList<T>
findAll(Sort)Loads all rows, sortedList<T>
findAll(Pageable)Loads one pagePage<T>
deleteById(id)Removes one rownothing
count()Counts rowslong

Common Mistakes

  • Calling `get()` on the Optional without checking. If the row is missing, it throws an exception. Use orElseThrow with a clear message or ifPresent.
  • Sorting by a column name. Sort.by("unit_price") fails. Use the Java field name, such as unitPrice.
  • Wrong id type in the generics. If the entity id is Long, write JpaRepository<Meal, Long>. A mismatch causes confusing errors.
  • Thinking `save` always inserts. With an existing id it updates. That is handy, but it can overwrite data if you send an old copy of an object.

Interview Questions

What is the difference between `CrudRepository` and `JpaRepository`?

Ans:JpaRepository extends the list-based CRUD and paging interfaces and adds JPA methods such as flush and saveAndFlush. It is the usual choice for JPA projects.

How does Spring create the repository implementation?

Ans:At startup it builds a proxy for your interface. Calls go to SimpleJpaRepository, which uses Hibernate to run them.

What does `save()` do for an entity that already has an id?

Ans:It updates the existing row. For an entity without an id, it inserts a new row.

Key Points to Remember

  • Extend JpaRepository<Entity, IdType> and Spring builds the implementation for you.
  • Use save, findById, findAll, deleteById and count for everyday jobs.
  • findById returns an Optional, so handle the empty case.
  • Use Sort and Pageable to sort and page results, and avoid loading whole tables.
  • The same save method inserts new rows and updates existing ones.

Frequently Asked Questions

Do I need to annotate a JpaRepository with @Repository?

No. Spring Data JPA detects interfaces that extend JpaRepository on its own, as long as they sit under your main application package.

What is the difference between save and saveAndFlush in JpaRepository?

save may keep changes in memory until the end of the transaction. saveAndFlush sends the SQL to the database at once. Most code needs plain save.

Can I add my own methods to a JpaRepository?

Yes. You can add derived query methods by naming them well, or write your own query with an annotation. Both are covered in the next topics.

Does JpaRepository work with any database?

Yes, it works with any database that Hibernate supports, such as H2, MySQL and PostgreSQL. Your repository code does not change.

Practice Problems

Try each problem on your own first. Each project has its own pom.xml, shown below.

Easy: Movie Watchlist

Night Owl Club keeps a watchlist of movies. Build an entity Movie (a title and a watched flag that starts as false) with a repository. Save three movies with saveAll, mark the second one as watched, delete the third one, and print the movies that are left with their status.

Use these titles: Paper Boats, Night Market, The Last Ferry.

Show answer
Update and insert both use save. The second save updates, because the loaded movie already has an id.

File: pom.xml

xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.nightowl</groupId> <artifactId>watchlist</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <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> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

File: WatchlistApplication.java in package com.nightowl.watchlist

java
package com.nightowl.watchlist; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class WatchlistApplication { public static void main(String[] args) { SpringApplication.run(WatchlistApplication.class, args); } }

File: Movie.java in package com.nightowl.watchlist

java
package com.nightowl.watchlist; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @Entity public class Movie { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private boolean watched; protected Movie() { } public Movie(String title) { this.title = title; } public void markWatched() { this.watched = true; } @Override public String toString() { return id + " " + title + (watched ? " [watched]" : " [to watch]"); } }

File: MovieRepository.java in package com.nightowl.watchlist

java
package com.nightowl.watchlist; import org.springframework.data.jpa.repository.JpaRepository; public interface MovieRepository extends JpaRepository<Movie, Long> { }

File: WatchlistRunner.java in package com.nightowl.watchlist

java
package com.nightowl.watchlist; import java.util.List; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @Component public class WatchlistRunner implements CommandLineRunner { private final MovieRepository repository; public WatchlistRunner(MovieRepository repository) { this.repository = repository; } @Override public void run(String... args) { repository.saveAll(List.of( new Movie("Paper Boats"), new Movie("Night Market"), new Movie("The Last Ferry"))); Movie second = repository.findById(2L).orElseThrow(); second.markWatched(); repository.save(second); repository.deleteById(3L); repository.findAll().forEach(System.out::println); } }

The output is:

text
1 Paper Boats [to watch] 2 Night Market [watched]

Medium: Paged Book Catalogue

A library wants to show its books two at a time, sorted by title. Save five books, then use a PageRequest of size 2 and loop through every page. For each page, print the page number (starting from 1) and the titles on it. Stop when there is no next page.

Use these titles: River of Stories, Coastal Winds, Night Train to Pune, The Tea Garden, Monsoon Diary.

Show answer
Each call to findAll(pageable) returns a Page. The loop asks for the next pageable until hasNext() is false.

File: pom.xml

xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.pageturn</groupId> <artifactId>catalogue</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <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> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

File: CatalogueApplication.java in package com.pageturn.catalogue

java
package com.pageturn.catalogue; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class CatalogueApplication { public static void main(String[] args) { SpringApplication.run(CatalogueApplication.class, args); } }

File: Book.java in package com.pageturn.catalogue

java
package com.pageturn.catalogue; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; protected Book() { } public Book(String title) { this.title = title; } public String getTitle() { return title; } }

File: BookRepository.java in package com.pageturn.catalogue

java
package com.pageturn.catalogue; import org.springframework.data.jpa.repository.JpaRepository; public interface BookRepository extends JpaRepository<Book, Long> { }

File: PageRunner.java in package com.pageturn.catalogue

java
package com.pageturn.catalogue; import java.util.List; import org.springframework.boot.CommandLineRunner; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; @Component public class PageRunner implements CommandLineRunner { private final BookRepository repository; public PageRunner(BookRepository repository) { this.repository = repository; } @Override public void run(String... args) { repository.saveAll(List.of( new Book("River of Stories"), new Book("Coastal Winds"), new Book("Night Train to Pune"), new Book("The Tea Garden"), new Book("Monsoon Diary"))); Pageable pageable = PageRequest.of(0, 2, Sort.by("title")); Page<Book> page; do { page = repository.findAll(pageable); System.out.println("Page " + (page.getNumber() + 1) + ":"); page.getContent().forEach(b -> System.out.println(" " + b.getTitle())); pageable = page.nextPageable(); } while (page.hasNext()); } }

The output is:

text
Page 1: Coastal Winds Monsoon Diary Page 2: Night Train to Pune River of Stories Page 3: The Tea Garden