JPA Entity Mapping – Tables, Columns and Ids

JPA Beginner 5 min read Published
In this lesson

By default JPA maps a class Course to a table Course and every field to a column of the same name. Real databases have naming conventions, constraints and special types. Annotations let you control all of it.

A fully mapped entity#

package com.vitalstack;

import jakarta.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;

@Entity
@Table(name = "students",
       uniqueConstraints = @UniqueConstraint(name = "uk_student_email", columnNames = "email"))
public class Student {

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

    @Column(name = "full_name", nullable = false, length = 100)
    private String name;

    @Column(nullable = false)
    private String email;

    @Enumerated(EnumType.STRING)              // store "ACTIVE", not 0
    @Column(length = 20)
    private Status status = Status.ACTIVE;

    private LocalDate birthDate;              // DATE column
    private LocalDateTime joinedAt;           // TIMESTAMP column

    @Lob                                       // large text (CLOB)
    private String bio;

    @Embedded                                  // columns of Address live in this table
    private Address address;

    @Transient                                 // NOT stored in the database
    private int unreadNotifications;

    protected Student() { }

    public Student(String name, String email, Address address) {
        this.name = name;
        this.email = email;
        this.address = address;
        this.joinedAt = LocalDateTime.now();
    }

    public Long getId() { return id; }
    public String getName() { return name; }
    public Status getStatus() { return status; }
    public Address getAddress() { return address; }
}
package com.vitalstack;

public enum Status { ACTIVE, SUSPENDED, GRADUATED }
package com.vitalstack;

import jakarta.persistence.Embeddable;

@Embeddable                                    // a value object: no table, no id of its own
public class Address {
    private String city;
    private String pin;

    protected Address() { }
    public Address(String city, String pin) { this.city = city; this.pin = pin; }
    public String getCity() { return city; }
}
package com.vitalstack;

import jakarta.persistence.*;

public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("school");
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        em.persist(new Student("Asha Kumar", "asha@example.com", new Address("Pune", "411001")));
        em.getTransaction().commit();
        Student s = em.createQuery("select s from Student s", Student.class).getSingleResult();
        System.out.println(s.getName() + " | " + s.getStatus() + " | " + s.getAddress().getCity());
        em.close();
        emf.close();
    }
}

Generated table (H2, reformatted for reading):

create table students (
    birthDate date,
    id bigint generated by default as identity,
    joinedAt timestamp(6),
    full_name varchar(100) not null,
    city varchar(255),
    email varchar(255) not null,
    pin varchar(255),
    bio clob,
    status enum ('ACTIVE','GRADUATED','SUSPENDED'),
    primary key (id),
    constraint uk_student_email unique (email))

The column type for status depends on the database: H2 and MySQL get an enum type, others a varchar.

Output: Asha Kumar | ACTIVE | Pune

@Table and @Column#

AttributeExampleEffect
@Table(name)"students"table name
@Column(name)"full_name"column name
nullablefalseNOT NULL
length100VARCHAR(100)
uniquetrueunique constraint
precision, scale10, 2for BigDecimal money: DECIMAL(10,2)
insertable, updatablefalseread-only columns

These constraints matter only for schema generation; they don’t validate data in Java. Use Bean Validation (@NotBlank, @Email, @Size) for that. You’ll use it in the Spring Boot course.

Id generation strategies#

StrategyHow it worksNotes
IDENTITYdatabase auto-increment columnsimple; MySQL’s natural choice; disables JDBC insert batching
SEQUENCEdatabase sequence objectbest for PostgreSQL/Oracle; supports batching
UUID@GeneratedValue(strategy = GenerationType.UUID) on a UUID idglobally unique; good for distributed systems and public ids
AUTO (default)provider choosesHibernate 6 picks SEQUENCE for most databases
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "student_seq")
@SequenceGenerator(name = "student_seq", sequenceName = "student_seq", allocationSize = 50)
private Long id;

allocationSize = 50 lets Hibernate reserve 50 ids per database round trip: much faster inserts.

Enums: always STRING#

@Enumerated(EnumType.STRING)
private Status status;

The default ORDINAL stores 0, 1, 2. If someone later inserts a new constant in the middle of the enum, every existing row silently changes meaning. STRING stores readable names and is safe.

Dates and times#

Use java.time types: they map automatically.

JavaSQL
LocalDateDATE
LocalTimeTIME
LocalDateTimeTIMESTAMP
Instant, OffsetDateTimeTIMESTAMP WITH TIME ZONE (provider dependent)

Avoid old java.util.Date and @Temporal in new code.

Embeddables: value objects#

Address has no identity of its own: it belongs to a student. @Embeddable + @Embedded store its fields as columns of the owner’s table. Reuse it in Teacher and Company too. Rename columns per use with @AttributeOverride.

@Transient#

Fields marked @Transient (or Java transient) aren’t persisted: perfect for computed or temporary values.

Field vs property access#

Where you put @Id decides the access type: on a field → JPA reads/writes fields directly (recommended); on a getter → JPA uses getters/setters. Don’t mix them in one entity.

Keep entities focused on data and simple rules. Don’t put HTTP or UI logic in them, and don’t return entities directly from public APIs; map them to DTOs/records (Spring Boot course).

Common mistakes#

  • Forgetting @Enumerated(EnumType.STRING).
  • Using double for money: use BigDecimal with precision/scale, or store paise in a long.
  • Column names that are SQL reserved words (order, user, group) → rename with @Table/@Column.
  • Expecting nullable = false to validate before saving (it only affects DDL; the database throws an exception).

Exercise#

  1. Map a Product entity: sku (unique, 20 chars), name (not null), price as BigDecimal(10,2), category enum, createdAt.
  2. Create an @Embeddable Money(amount, currency) and use it in Order twice (subtotal, tax) with @AttributeOverrides.
  3. Switch Student to a UUID id and check the generated DDL.
Hint for 2

@AttributeOverrides({ @AttributeOverride(name = "amount", column = @Column(name = "tax_amount")), @AttributeOverride(name = "currency", column = @Column(name = "tax_currency")) }) on the second field.

Interview corner#

  • @Column(nullable=false) vs @NotNull? The first only shapes the DDL; the second is Bean Validation that checks before persisting.
  • IDENTITY vs SEQUENCE? IDENTITY uses auto-increment (no batching); SEQUENCE fetches ids from a sequence (batch-friendly with allocationSize).
  • Why EnumType.STRING? ORDINAL breaks when enum constants are reordered.
  • What is @Embeddable? A value type whose fields are stored in the owning entity’s table.

Summary#

  • @Table, @Column control names and constraints; @Id + @GeneratedValue choose ids.
  • Map enums as STRING, dates with java.time, big text with @Lob.
  • @Embeddable value objects group columns; @Transient skips fields.

Next: the EntityManager and the four states of an entity, animated.

Quick quiz

3 questions. Check what stuck and earn 2 XP for each right answer.

1Which enum mapping is safe when constants are reordered?
2What is @Embeddable for?
3Which id strategy allows JDBC insert batching?

Progress is saved in this browser. Create a free account to keep it on every device.