Skip to content

Spring Boot JPA (Part 5) | Mapping Entities to Tables with JPA Annotations

The Passport and Identity Document Analogy

Imagine government offices maintaining citizens' official identity records. In the computer database, a citizen record has a unique national identity number, birth date, home address, and civil status. However, when you receive your physical passport booklet, the format is customized for human reading: the address might be broken into street, city, and postal code; certain internal security clearance codes are withheld; and official stamps use standardized dates.

In Java enterprise applications, Object Relational Mapping (ORM) is that translation mechanism between Java objects in heap memory and relational database tables on disk. Relational databases speak tables, foreign keys, constraints, and rows. Java applications speak classes, objects, interfaces, and references. JPA annotations are the formal mapping instructions that dictate how each Java property translates into database columns, types, and constraints.

This lecture covers essential entity mapping annotations: @Entity, @Table, primary key strategies with @Id and @GeneratedValue, column constraints with @Column, composite value types using @Embeddable and @Embedded, and handling enums with @Enumerated.


Core Class Level Annotations: @Entity and @Table

1. @Entity

Marks a Java class as a JPA entity, informing Hibernate that this class maps to a relational database table:

java
package com.example.orderservice.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public class Customer {
    @Id
    private Long id;
    private String name;
}

Rules for @Entity classes:

  • Must have a no argument constructor (can be protected or public).
  • Must have a primary key annotated with @Id.
  • Cannot be declared final.
  • Fields should use standard getters and setters.

2. @Table

Specifies the exact database table name and constraints. If @Table is omitted, JPA defaults the table name to the uncapitalized class name.

java
package com.example.orderservice.entity;

import jakarta.persistence.*;

@Entity
@Table(
    name = "tbl_customers",
    uniqueConstraints = {
        @UniqueConstraint(name = "uk_customer_email", columnNames = {"email_address"})
    },
    indexes = {
        @Index(name = "idx_customer_name", columnList = "full_name")
    }
)
public class Customer {
    @Id
    private Long id;
}

Primary Key Generation Strategies (@GeneratedValue)

JPA provides four primary key generation strategies via @GeneratedValue(strategy = ...):

StrategyBehavior & Underlying Database MechanismBest For
GenerationType.IDENTITYUses database auto increment columns (e.g. SERIAL in PostgreSQL, AUTO_INCREMENT in MySQL)MySQL, SQLite, simple schemas
GenerationType.SEQUENCEUses database sequences (CREATE SEQUENCE ...). Allocates ID blocks in memory using allocationSizePostgreSQL, Oracle (Enterprise default)
GenerationType.UUIDGenerates 128 bit UUIDs in Java or database before insertionDistributed systems, microservices
GenerationType.AUTODelegates choice to Hibernate based on dialectQuick prototyping; avoid in production

Why GenerationType.SEQUENCE Is Preferred for Batch Performance:

With GenerationType.IDENTITY, Hibernate cannot know the generated ID until the row is physically inserted with INSERT INTO. This disables JDBC batch insertions because Hibernate must execute an immediate insert for every single entity to retrieve its generated key.

With GenerationType.SEQUENCE, Hibernate queries the sequence upfront for a block of fifty IDs (allocationSize = 50), assigns IDs in memory, and batches fifty INSERT statements together into a single database network call.

java
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "customer_seq_gen")
@SequenceGenerator(
    name = "customer_seq_gen",
    sequenceName = "customer_sequence",
    initialValue = 1,
    allocationSize = 50
)
private Long id;

Column Mapping and Constraints (@Column)

@Column customizes the mapping between a Java field and its corresponding database column:

java
@Column(
    name = "email_address",
    nullable = false,
    unique = true,
    length = 120,
    updatable = false // Field cannot be modified in UPDATE queries
)
private String email;

Key Attributes of @Column:

  • name: Explicit column name in database table.
  • nullable: If false, adds a NOT NULL constraint to DDL schema.
  • unique: Adds a unique constraint to DDL generation.
  • length: Column character size (default is 255).
  • updatable = false: Protects sensitive fields (such as creation timestamps or account numbers) from being updated after creation.

Handling Enums with @Enumerated

In Java, enums represent fixed categories. How does JPA store an enum in a SQL database?

1. EnumType.ORDINAL (Default — DANGEROUS!)

Stores the zero based index of the enum constant:

java
public enum OrderStatus {
    PENDING,    // 0
    CONFIRMED,  // 1
    SHIPPED     // 2
}

If you use EnumType.ORDINAL, PENDING is stored as 0, CONFIRMED as 1. The Pitfall: If another developer later inserts a new enum value at the top (CANCELLED, PENDING, CONFIRMED), the ordinal indices shift: PENDING becomes 1, and existing database rows now interpret old pending orders as cancelled!

2. EnumType.STRING (The Industry Standard)

Stores the literal name of the enum constant as a VARCHAR:

java
@Enumerated(EnumType.STRING)
@Column(name = "order_status", length = 20)
private OrderStatus status;

Now, CONFIRMED is stored as "CONFIRMED". Reordering enum constants in Java code has zero impact on database integrity.


Transient Fields: @Transient

If an entity contains calculated fields that should exist in Java memory but must never be saved to the database, mark them with @Transient:

java
@Entity
public class Employee {

    @Id
    private Long id;

    private LocalDate birthDate;

    // Calculated on the fly; no column in database table!
    @Transient
    private int age;

    public int getAge() {
        return Period.between(birthDate, LocalDate.now()).getYears();
    }
}

Component Mapping: @Embeddable and @Embedded

Sometimes a domain object contains a cohesive group of fields that conceptually belong together, but do not warrant their own separate database table with foreign keys.

For example, an Address (street, city, zip code) used by both Customer and Company:

java
package com.example.orderservice.entity;

import jakarta.persistence.Embeddable;

// 1. Mark component as Embeddable
@Embeddable
public class Address {

    private String street;
    private String city;
    private String zipCode;

    public Address() {}

    public Address(String street, String city, String zipCode) {
        this.street = street;
        this.city = city;
        this.zipCode = zipCode;
    }

    // Getters and setters
}

In the parent entity, use @Embedded:

java
package com.example.orderservice.entity;

import jakarta.persistence.*;

@Entity
@Table(name = "customers")
public class Customer {

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

    private String fullName;

    // Fields of Address are mapped directly into columns of the 'customers' table
    @Embedded
    private Address billingAddress;

    // To reuse Address for shipping address in the same table, override column names:
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "street", column = @Column(name = "ship_street")),
        @AttributeOverride(name = "city", column = @Column(name = "ship_city")),
        @AttributeOverride(name = "zipCode", column = @Column(name = "ship_zip"))
    })
    private Address shippingAddress;
}

Database schema produced:

sql
CREATE TABLE customers (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    full_name VARCHAR(255),
    street VARCHAR(255),
    city VARCHAR(255),
    zip_code VARCHAR(255),
    ship_street VARCHAR(255),
    ship_city VARCHAR(255),
    ship_zip VARCHAR(255)
);

Notice that there is only one database table. The Java object model remains clean and modular with reusable Address components, while the database schema remains flat without unnecessary table joins.


Interview Questions & Pitfalls

Q1: Why is EnumType.STRING preferred over EnumType.ORDINAL for mapping Java enums?

EnumType.ORDINAL saves the integer index of the enum constant. If new constants are inserted or reordered in the Java enum file, the index values shift, corrupting existing database rows. EnumType.STRING persists the literal name string, ensuring database integrity regardless of enum order changes in code.

Q2: Why does GenerationType.IDENTITY disable JDBC batch inserts in Hibernate?

GenerationType.IDENTITY relies on the database allocating the primary key during the execution of an INSERT statement. Hibernate cannot know the entity's generated ID until the row is physically inserted. Therefore, it must execute each INSERT immediately, preventing multiple inserts from being grouped into a single batch network call.

Q3: What is the purpose of @AttributeOverride when working with @Embedded objects?

When an entity embeds the same @Embeddable class multiple times (such as having both a billing address and a shipping address), both embedded fields would default to mapping to identical column names (street, city), causing a schema column collision. @AttributeOverride allows you to redefine column names for that specific embedded property.

Q4: What is the difference between @Transient in JPA and transient in core Java?

jakarta.persistence.Transient tells JPA and Hibernate to ignore the field when mapping and saving to the database. The Java language keyword transient tells the standard Java serialization mechanism (java.io.Serializable) to ignore the field when serializing the object to a binary stream.

Q5: Why must JPA entities provide a no argument constructor?

When Hibernate queries the database, it uses Java Reflection to instantiate the entity before populating fields from the SQL ResultSet. The reflection API invokes the default zero argument constructor (Class.getDeclaredConstructor().newInstance()). If no default constructor exists, entity instantiation fails with an InstantiationException.