JPA Entities & Repositories
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
| Piece | Role |
|---|---|
| JPA (Jakarta Persistence 3.2) | The standard API: annotations such as @Entity, and the EntityManager |
| Hibernate ORM 7 | The implementation that turns objects into SQL |
| Spring Data JPA | Writes repository classes for you from interfaces |
| HikariCP | Connection pool, so connections are reused, not opened per query |
| H2 | A 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"):
<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:
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
}@Entitymarks the class;@Table(name = "players")names the table (otherwise it would beplayer).@Idmarks the primary key.GenerationType.IDENTITYlets the database number new rows (an auto-increment column). The type isLong, notlong, so a new, unsaved player hasid == null.@Columnsets constraints such asnullable = falseand 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
protectedno-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:
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>:
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:
| Method | Does |
|---|---|
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:
@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:
curl http://localhost:8080/api/players/1{"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:
@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.persistenceimports from old tutorials. Spring Boot 3 and 4 usejakarta.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=updatein production. It cannot rename columns or migrate data safely; use Flyway or Liquibase.
Key points
- Spring Data JPA uses Hibernate (JPA) to map
@Entityclasses to tables; addspring-boot-starter-data-jpaand a database driver. - Every entity needs
@Id, usually@GeneratedValue(strategy = GenerationType.IDENTITY)with aLongid, and a protected no-argument constructor. - Store enums with
@Enumerated(EnumType.STRING). - An interface extending
JpaRepository<Player, Long>gives yousave,findById,findAll,countanddeleteByIdwith 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
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?
The type parameters are the entity class and the type of its primary key.
2. Why should an enum field use @Enumerated(EnumType.STRING)?
The default stores positions, which change meaning when constants move.
3. Why does an entity need a protected no-argument constructor?
Hibernate creates the object first, then fills its fields from the row.
4. Inside a @Transactional method you load a player and call setRuns(500). What happens?
Dirty checking saves changes to managed entities when the transaction commits.
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
