JPA, Hibernate and entities
Almost every backend stores data in a relational database, and in Spring you talk to it through JPA (the Jakarta Persistence API) with Hibernate as the implementation. JPA lets you work with Java objects — entities — and have them mapped to database tables, so you rarely write SQL by hand. But "rarely write SQL" does not mean "SQL disappears": Hibernate is generating it for you, and this course's promise is that you understand what it generates. This lesson is entities and the mapping, verified against a running Dakiya with a real database.
What JPA and Hibernate are
Two names to keep straight:
- JPA is the specification — a standard API (
jakarta.persistence.*) for mapping Java objects to database tables (an ORM, object-relational mapping). It defines the annotations (@Entity,@Id, …) and theEntityManager. - Hibernate is the most common implementation of that spec — the actual engine that turns your
entities and queries into SQL and runs them. Spring Boot's
starter-data-jpabrings Hibernate by default.
So you code against JPA (the standard annotations), and Hibernate does the work underneath. The mental model: you describe your data as annotated Java classes; Hibernate generates and runs the SQL to persist and load them. It is the Java equivalent of Django's ORM — objects in your code, tables in the database, a mapping layer between.
An entity is a class mapped to a table
An entity is a class annotated @Entity; each instance maps to a row, each field to a column. Dakiya's
Parcel:
import jakarta.persistence.*;
@Entity
public class Parcel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // the database assigns the id
private Long id;
private String trackingCode;
private String recipientName;
private String destinationCity;
@Enumerated(EnumType.STRING) // store the enum as its name, not its ordinal
private Status status = Status.BOOKED;
public enum Status { BOOKED, IN_TRANSIT, DELIVERED, CANCELLED }
protected Parcel() {} // JPA needs a no-arg constructor
public Parcel(String trackingCode, String recipientName, String destinationCity) { ... }
// getters...
}
From this class, Hibernate manages a parcel table with matching columns. Verified against a running app:
saving a Parcel inserted a row and loading it back returned the object. The essential annotations:
@Entity— this class is a persistent entity (a table).@Id— this field is the primary key.@GeneratedValue(strategy = IDENTITY)— the database generates the id (an auto-increment column); you do not set it.@Enumerated(EnumType.STRING)— always map enums asSTRING, not the defaultORDINAL. Ordinal stores the enum's position (0, 1, 2…), so reordering or inserting an enum value silently corrupts existing data.STRINGstores the name ("BOOKED") — stable and readable. This is a classic data-corruption trap, and the fix is one annotation.
Field mapping, and columns you control
By default Hibernate maps each field to a column of a sensible type and name (camelCase field → snake_case or same-name column, depending on the naming strategy). You override when needed:
@Column(nullable = false, length = 20, unique = true)
private String trackingCode; // NOT NULL, VARCHAR(20), UNIQUE
@Column(name = "recipient_name")
private String recipientName; // explicit column name
@Column controls nullability, length, uniqueness and name. A field with no annotation still maps (with
defaults); @Column is for when you need to constrain it. Types map naturally — String → VARCHAR, Long
→ BIGINT, LocalDateTime → a timestamp, BigDecimal → an exact decimal (use BigDecimal for money, never
double). A field marked @Transient is not persisted (a computed value you do not store).
The no-arg constructor, and why entities look the way they do
Two JPA requirements that surprise newcomers, both visible in Parcel:
- A no-argument constructor is required (JPA instantiates entities reflectively when loading rows). Make
it
protectedso your own code uses the meaningful constructor but JPA can still call it — asParceldoes. - Fields, not the constructor, define the mapping. JPA sets fields directly (via reflection) when loading, so a loaded entity does not go through your constructor. This is why entities are typically mutable JavaBeans with getters (and why records do not work as entities — they are immutable and have no no-arg constructor).
These constraints are why entities look like plain mutable classes rather than the immutable records you use for DTOs — a good reason the two are separate types (the DTO lesson). Entities are shaped for the persistence layer; DTOs for the API.
You still get SQL — and should look at it
The whole point of the course: JPA does not hide SQL, it generates it, and you should see it. Turn on SQL
logging in application.properties:
spring.jpa.show-sql=true # log the SQL Hibernate generates
spring.jpa.properties.hibernate.format_sql=true # readable formatting
Now every insert, select and update Hibernate runs is printed. Saving a Parcel logs an INSERT INTO parcel ...; loading by id logs a SELECT ... WHERE id = ?. Watching this is how you learn what your code
actually does to the database — and it is essential for the N+1 lesson, where the number of SQL statements
is the whole story. Keep SQL logging on in development. JPA's convenience is real, but an engineer who
cannot see the SQL their entities generate is exactly the "adds an annotation and hopes" developer this
course exists to prevent you becoming.
Check your work
JPA vs Hibernate. JPA is the specification (the annotations, EntityManager); Hibernate is the
implementation that generates and runs the SQL. You code against JPA; Hibernate does the work.
An entity. A @Entity class mapped to a table; @Id marks the primary key; @GeneratedValue(IDENTITY)
lets the database assign it. Verified: saving inserts a row, loading returns the object.
@Enumerated(EnumType.STRING). Always map enums as STRING (the name), never ORDINAL (the position) —
ordinal corrupts data when enum values are reordered.
Field/column mapping. Fields map to columns by default; @Column controls name/nullability/length/
uniqueness; BigDecimal for money; @Transient for non-persisted fields.
JPA requirements. A (protected) no-arg constructor is required, and JPA sets fields reflectively — which is why entities are mutable classes with getters, and why records (immutable) are used for DTOs but not entities.
See the SQL. spring.jpa.show-sql=true logs the generated SQL; keep it on in development — JPA generates
SQL, it does not abolish it, and understanding it is the point.
Practice
- Create a
Parcel@Entitywith@Id @GeneratedValue; save one and confirm (withshow-sql=true) theINSERTHibernate generates. - Map the status enum with
@Enumerated(EnumType.STRING); inspect the stored value. Switch toORDINAL, reorder the enum, and reason about how existing rows are now wrong. - Add
@Column(nullable=false, unique=true)totrackingCodeand confirm the generated DDL / a duplicate insert failing. - Try to make an entity a
recordand observe why it fails (no no-arg constructor, immutable). - Add a
@Transientcomputed field and confirm it is not persisted (no column). - Turn on
show-sqlandformat_sql, do a save and a load, and read the exact SQL — internalise that JPA generates it.
Official documentation
- Spring — Working with SQL databases / JPA — JPA in Spring Boot.
- Jakarta Persistence — annotations —
@Entity,@Id,@Column,@Enumerated. - Hibernate ORM — User Guide — The implementation underneath.
Next: Spring Data repositories.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship