RizTech Academy logo
RizTech Academy
Data Access with JPALesson 1 of 635 min

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 the EntityManager.
  • 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-jpa brings 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 as STRING, not the default ORDINAL. Ordinal stores the enum's position (0, 1, 2…), so reordering or inserting an enum value silently corrupts existing data. STRING stores 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 protected so your own code uses the meaningful constructor but JPA can still call it — as Parcel does.
  • 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

  1. Create a Parcel @Entity with @Id @GeneratedValue; save one and confirm (with show-sql=true) the INSERT Hibernate generates.
  2. Map the status enum with @Enumerated(EnumType.STRING); inspect the stored value. Switch to ORDINAL, reorder the enum, and reason about how existing rows are now wrong.
  3. Add @Column(nullable=false, unique=true) to trackingCode and confirm the generated DDL / a duplicate insert failing.
  4. Try to make an entity a record and observe why it fails (no no-arg constructor, immutable).
  5. Add a @Transient computed field and confirm it is not persisted (no column).
  6. Turn on show-sql and format_sql, do a save and a load, and read the exact SQL — internalise that JPA generates it.

Official documentation

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