EthicsComputerCourses
Join training +91 90532 10052

JPA Entities & Repositories

Spring Boot · Lesson 13 of 29 Intermediate 7 min read Updated

So far the academy keeps players in a Map that disappears every time the app restarts. Real applications keep data in a database. In Java, the standard way to work with relational databases is JPA (Jakarta Persistence), and Spring Data JPA makes it so short that a whole data-access layer can be one interface with no implementation.

In this lesson you will add the JPA starter, turn Player into an entity mapped to a table, see the SQL that Hibernate generates, and use a repository for create, read, update and delete.

The pieces

PieceRole
JPA (Jakarta Persistence 3.2)The standard API: annotations such as @Entity, and the EntityManager
Hibernate ORM 7The implementation that turns objects into SQL
Spring Data JPAWrites repository classes for you from interfaces
HikariCPConnection pool, so connections are reused, not opened per query
H2A small Java database that runs in memory, ideal for learning

ORM (object-relational mapping) means each object maps to a table row and each field to a column. Add two dependencies (Initializr: "Spring Data JPA" and "H2 Database"):

XML
<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>

With H2 on the classpath and no database URL configured, Spring Boot starts an in-memory database automatically. The next lesson, Database Setup, covers H2 settings, MySQL and PostgreSQL.

An entity

An entity is a class whose objects are stored in the database. JPA needs a few things that records cannot give (a no-argument constructor and changeable fields), so entities are ordinary classes:

Java
package in.cricket.academy.player;

import java.time.LocalDate;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "players")
public class Player {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 60)
    private String name;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 20)
    private PlayerRole role;

    private int age;

    @Column(nullable = false, length = 40)
    private String city;

    private int runs;

    private LocalDate joinedOn;

    protected Player() {
        // required by JPA
    }

    public Player(String name, PlayerRole role, int age, String city, LocalDate joinedOn) {
        this.name = name;
        this.role = role;
        this.age = age;
        this.city = city;
        this.joinedOn = joinedOn;
    }

    public Long getId() { return id; }
    public String getName() { return name; }
    public int getRuns() { return runs; }
    public void setRuns(int runs) { this.runs = runs; }
    // other getters and setters left out
}
  • @Entity marks the class; @Table(name = "players") names the table (otherwise it would be player).
  • @Id marks the primary key. GenerationType.IDENTITY lets the database number new rows (an auto-increment column). The type is Long, not long, so a new, unsaved player has id == null.
  • @Column sets constraints such as nullable = false and string length. Fields without it still become columns.
  • @Enumerated(EnumType.STRING) stores the enum's name. Never rely on the default (ORDINAL, the position number): adding a new role in the middle would silently change the meaning of every stored row.
  • The protected no-argument constructor is for JPA only; your code uses the real constructor.

Spring Boot converts camelCase field names to snake_case columns: joinedOn becomes joined_on. Entities are found by scanning the main class's package and its sub-packages, the same rule as components.

The schema Hibernate creates

For an embedded database, Spring Boot sets spring.jpa.hibernate.ddl-auto=create-drop: Hibernate creates the tables at startup and drops them at shutdown. Run once with --spring.jpa.show-sql=true to watch:

Output
Hibernate: create table players (age integer not null, joined_on date, runs integer not null, id bigint generated by default as identity, team_id bigint, city varchar(40) not null, name varchar(60) not null, role enum ('ALL_ROUNDER','BATTER','BOWLER','WICKET_KEEPER') not null, primary key (id))

(The team_id column comes from a relationship you will add in Entity Relationships.) Primitive int fields became not null, because a primitive can never be empty. ddl-auto is perfect for learning; for real databases you will switch it off and use migrations.

A repository

Now the best part. Declare an interface that extends JpaRepository<EntityType, IdType>:

Java
package in.cricket.academy.player;

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

public interface PlayerRepository extends JpaRepository<Player, Long> {
}

That is the whole data-access layer. At startup, Spring Data creates a class that implements this interface and registers it as a bean. You inherit methods such as:

MethodDoes
save(player)INSERT a new entity or UPDATE an existing one; returns the saved entity
findById(id)SELECT by primary key; returns Optional<Player>
findAll(), findAll(Sort), findAll(Pageable)SELECT many rows
count(), existsById(id)Count or check
deleteById(id), delete(player)DELETE

Inject the repository into a service like any other bean:

Java
@Service
public class PlayerService {

    private final PlayerRepository players;

    public PlayerService(PlayerRepository players) {
        this.players = players;
    }

    public Player register(String name, PlayerRole role, int age, String city) {
        return players.save(new Player(name, role, age, city, LocalDate.now()));
    }

    public Optional<Player> find(long id) {
        return players.findById(id);
    }
}

save() runs an INSERT and returns the entity with its new id. Calling the API with the academy's sample data loaded:

Terminal
curl http://localhost:8080/api/players/1
Output
{"id":1,"name":"Aarav Patil","role":"BATTER","age":15,"city":"Pune","runs":412,"team":"Deccan Strikers"}

The JSON comes from a response record built from the entity, a pattern explained in Service Layer & DTOs.

The persistence context

Inside a transaction, Hibernate keeps every entity it loaded in a persistence context (a first-level cache). It notices changes you make to those objects and writes them back at commit, a feature called dirty checking:

Java
@Transactional
public void addRuns(long id, int runs) {
    Player player = players.findById(id).orElseThrow();
    player.setRuns(player.getRuns() + runs);
    // no save() needed: the UPDATE runs when the transaction commits
}

This surprises many beginners in both directions: changes are saved without save(), and changes made outside a transaction are not. Transactions explains the rules.

Common mistakes

  • Using a record as an entity. JPA needs a no-argument constructor and mutable fields. Use records for DTOs, classes for entities.
  • EnumType.ORDINAL (the default). Reordering the enum corrupts stored data. Always write @Enumerated(EnumType.STRING).
  • javax.persistence imports from old tutorials. Spring Boot 3 and 4 use jakarta.persistence.
  • Entities outside the scanned packages. They are not mapped, and you get "Not a managed type". Keep them under the main class's package.
  • Relying on ddl-auto=update in production. It cannot rename columns or migrate data safely; use Flyway or Liquibase.

Key points

  • Spring Data JPA uses Hibernate (JPA) to map @Entity classes to tables; add spring-boot-starter-data-jpa and a database driver.
  • Every entity needs @Id, usually @GeneratedValue(strategy = GenerationType.IDENTITY) with a Long id, and a protected no-argument constructor.
  • Store enums with @Enumerated(EnumType.STRING).
  • An interface extending JpaRepository<Player, Long> gives you save, findById, findAll, count and deleteById with no implementation code.
  • Inside a transaction, changes to loaded entities are saved automatically (dirty checking).

Exercise

The academy sells kit. Create an entity KitItem stored in the table kit_items with: an id, a name (required, up to 50 characters), a size enum (S, M, L) stored as text, a price in whole rupees (required), and inStock (true or false). Then create its repository and write a service method addItem(String name, KitSize size, int price) that saves an item in stock and returns its id.

Show solution
Java
public enum KitSize { S, M, L }

@Entity
@Table(name = "kit_items")
public class KitItem {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 50)
    private String name;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false, length = 2)
    private KitSize size;

    @Column(nullable = false)
    private int price;          // whole rupees

    private boolean inStock;

    protected KitItem() {
    }

    public KitItem(String name, KitSize size, int price, boolean inStock) {
        this.name = name;
        this.size = size;
        this.price = price;
        this.inStock = inStock;
    }

    public Long getId() { return id; }
}

public interface KitItemRepository extends JpaRepository<KitItem, Long> {
}

@Service
public class KitService {

    private final KitItemRepository items;

    public KitService(KitItemRepository items) {
        this.items = items;
    }

    public long addItem(String name, KitSize size, int price) {
        return items.save(new KitItem(name, size, price, true)).getId();
    }
}

save() returns the stored entity with the id the database generated. Storing money as whole rupees in an int avoids rounding problems for a shop like this; use BigDecimal when you need paise.

Quick Quiz

1. What does JpaRepository<Player, Long> describe?

2. Why should an enum field use @Enumerated(EnumType.STRING)?

3. Why does an entity need a protected no-argument constructor?

4. Inside a @Transactional method you load a player and call setRuns(500). What happens?

Learn Spring Boot with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call