Basic Entity Mapping: @Entity, @Table, @Id, @Column

Introduction

Entity mapping is the process of telling Hibernate how your Java class corresponds to a database table, and how each field maps to a column. JPA provides a rich set of annotations for this — from the minimal @Entity and @Id to the detailed @Column with constraints.

This article covers every annotation and attribute you need to map entities precisely and confidently.


@Entity

@Entity marks a class as a JPA entity — a Java object that maps to a database table. It is the minimum required annotation.

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

Rules for entity classes

  • Must have a no-argument constructor (can be protected — Hibernate uses it internally)
  • Must have an @Id field (primary key)
  • Must not be final — Hibernate generates subclass proxies for lazy loading
  • Fields should not be final — Hibernate needs to set them via reflection
@Entity
public class Product {

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

    private String name;
    private BigDecimal price;

    // Required by JPA — can be package-private or protected
    protected Product() {}

    // Your preferred constructor
    public Product(String name, BigDecimal price) {
        this.name = name;
        this.price = price;
    }

    // getters and setters
}

@Table

@Table is optional. Without it, Hibernate uses the class name as the table name. With it, you control the table name, schema, catalog, and constraints.

@Entity
@Table(
    name = "products",
    schema = "ecommerce",          // database schema (optional)
    uniqueConstraints = {
        @UniqueConstraint(
            name = "uq_product_sku",
            columnNames = {"sku"}
        ),
        @UniqueConstraint(
            name = "uq_product_name_category",
            columnNames = {"name", "category_id"}
        )
    },
    indexes = {
        @Index(name = "idx_product_status", columnList = "status"),
        @Index(name = "idx_product_category", columnList = "category_id")
    }
)
public class Product {
    // ...
}

@UniqueConstraint and @Index inside @Table are used when Hibernate generates the DDL (with ddl-auto=create or update). When using Flyway/Liquibase, declare these in your migration scripts instead.


@Id

Every entity must have exactly one field annotated with @Id. This field maps to the primary key column.

@Id
private Long id;

Supported @Id types:

  • Long / long
  • Integer / int
  • String
  • UUID
  • BigDecimal / BigInteger (rare)

Long (the wrapper, not primitive long) is recommended — it allows null to distinguish new (unsaved) entities from saved ones with id = 0.


@GeneratedValue

@GeneratedValue specifies how the primary key is auto-generated.

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
StrategyHow it worksBest for
IDENTITYDatabase auto-increment columnMySQL, SQL Server, H2
SEQUENCEDatabase sequence objectPostgreSQL, Oracle
TABLESeparate key table (portable but slow)Avoid unless necessary
AUTOHibernate picks based on dialectNot recommended — unpredictable
UUIDJava-generated UUIDDistributed systems, no DB dependency

For MySQL, IDENTITY is the standard choice:

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

Hibernate generates: id BIGINT NOT NULL AUTO_INCREMENT

For UUID primary keys:

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

UUID primary keys are covered in depth in Article 6.


@Column

@Column maps a field to a specific column, with control over name, constraints, and SQL type.

@Entity
@Table(name = "products")
public class Product {

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

    @Column(name = "product_name", nullable = false, length = 255)
    private String name;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;

    @Column(name = "stock_quantity", nullable = false, columnDefinition = "INT DEFAULT 0")
    private Integer stock;

    @Column(length = 20, nullable = false)
    private String status;

    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @Column(name = "updated_at", nullable = false)
    private LocalDateTime updatedAt;
}

@Column attributes

AttributeTypeDefaultMeaning
nameStringfield nameColumn name in the database
nullablebooleantrueWhether NULL is allowed
uniquebooleanfalseAdd a UNIQUE constraint
lengthint255VARCHAR length
precisionint0Total digits for DECIMAL
scaleint0Digits after decimal point
insertablebooleantrueInclude in INSERT statements
updatablebooleantrueInclude in UPDATE statements
columnDefinitionStringOverride the SQL column type

nullable = false

This adds a NOT NULL constraint to the DDL (when Hibernate manages the schema) and causes Hibernate to validate before sending SQL:

@Column(nullable = false)
private String name;

For runtime validation, also add Bean Validation:

@Column(nullable = false)
@NotBlank
private String name;

@Column(nullable = false) is a schema-level constraint; @NotBlank is an application-level constraint. Use both.

updatable = false

Use this for immutable columns — set once on INSERT, never changed:

@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;

Hibernate will not include created_at in UPDATE statements.

columnDefinition

Override the exact SQL type for the column:

@Column(columnDefinition = "TEXT")
private String description;

@Column(columnDefinition = "TINYINT(1)")
private Boolean active;

@Column(columnDefinition = "DECIMAL(10,2) DEFAULT 0.00")
private BigDecimal total;

Use columnDefinition sparingly — it ties you to a specific database dialect. Prefer Flyway migrations for complex column types.


Complete Entity: Product

Putting it all together with the e-commerce Product entity:

package com.devopsmonk.jpademo.domain;

import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Entity
@Table(
    name = "products",
    indexes = {
        @Index(name = "idx_product_status", columnList = "status"),
        @Index(name = "idx_product_category_id", columnList = "category_id")
    }
)
@Getter
@Setter
@NoArgsConstructor
public class Product {

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

    @Column(nullable = false, length = 255)
    @NotBlank
    private String name;

    @Column(columnDefinition = "TEXT")
    private String description;

    @Column(nullable = false, precision = 10, scale = 2)
    @NotNull
    @DecimalMin("0.00")
    private BigDecimal price;

    @Column(name = "stock_quantity", nullable = false)
    @Min(0)
    private Integer stock = 0;

    @Column(length = 20, nullable = false)
    @NotBlank
    private String status = "ACTIVE";

    // Relationship to be added in Article 10
    private Long categoryId;

    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @Column(name = "updated_at", nullable = false)
    private LocalDateTime updatedAt;
}

Naming Strategies

When you do not use @Column(name = "..."), Hibernate derives the column name from the field name using a NamingStrategy.

By default, Spring Boot uses SpringPhysicalNamingStrategy, which converts camelCase to snake_case:

Field nameColumn name
productNameproduct_name
stockQuantitystock_quantity
createdAtcreated_at

This is the Spring Boot convention. You can override it:

# Use Hibernate's default (unchanged camelCase)
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

# Spring Boot's snake_case strategy (default)
spring.jpa.hibernate.naming.physical-strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy

In practice, rely on the naming strategy instead of specifying name on every @Column. Only use @Column(name = "...") when the naming convention doesn’t produce the right column name.


@Transient

Fields annotated with @Transient are not persisted to the database:

@Entity
public class Product {

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

    private BigDecimal price;
    private Integer stock;

    // Not stored in DB — computed at runtime
    @Transient
    private boolean inStock;

    public boolean isInStock() {
        return stock != null && stock > 0;
    }
}

Java’s transient keyword also works for JPA (it marks fields as non-persistent), but @Transient is preferred for clarity.


Complete Category Entity

The Category entity from this series:

package com.devopsmonk.jpademo.domain;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;

@Entity
@Table(
    name = "categories",
    uniqueConstraints = {
        @UniqueConstraint(name = "uq_category_slug", columnNames = "slug")
    }
)
@Getter
@Setter
@NoArgsConstructor
public class Category {

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

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

    @Column(nullable = false, length = 100)
    @NotBlank
    private String slug;

    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @Column(name = "updated_at", nullable = false)
    private LocalDateTime updatedAt;
}

Key Takeaways

  • @Entity marks a class as a JPA entity — it must have a no-arg constructor and an @Id field
  • @Table is optional — use it when you need to control the table name, add unique constraints, or define indexes
  • @Id marks the primary key field; @GeneratedValue specifies how the key is generated
  • @Column controls the column name, nullability, length, precision, and more
  • updatable = false on createdAt prevents it from appearing in UPDATE statements
  • Spring Boot uses snake_case naming by default — createdAt maps to created_at automatically
  • @Transient excludes a field from persistence

What’s Next

Article 6 covers primary keys and generated values in depth — IDENTITY vs SEQUENCE strategies, UUID primary keys, composite keys with @EmbeddedId, and the performance implications of each choice.