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
@Idfield (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/longInteger/intStringUUIDBigDecimal/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;
| Strategy | How it works | Best for |
|---|---|---|
IDENTITY | Database auto-increment column | MySQL, SQL Server, H2 |
SEQUENCE | Database sequence object | PostgreSQL, Oracle |
TABLE | Separate key table (portable but slow) | Avoid unless necessary |
AUTO | Hibernate picks based on dialect | Not recommended — unpredictable |
UUID | Java-generated UUID | Distributed 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
| Attribute | Type | Default | Meaning |
|---|---|---|---|
name | String | field name | Column name in the database |
nullable | boolean | true | Whether NULL is allowed |
unique | boolean | false | Add a UNIQUE constraint |
length | int | 255 | VARCHAR length |
precision | int | 0 | Total digits for DECIMAL |
scale | int | 0 | Digits after decimal point |
insertable | boolean | true | Include in INSERT statements |
updatable | boolean | true | Include in UPDATE statements |
columnDefinition | String | — | Override 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 name | Column name |
|---|---|
productName | product_name |
stockQuantity | stock_quantity |
createdAt | created_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
@Entitymarks a class as a JPA entity — it must have a no-arg constructor and an@Idfield@Tableis optional — use it when you need to control the table name, add unique constraints, or define indexes@Idmarks the primary key field;@GeneratedValuespecifies how the key is generated@Columncontrols the column name, nullability, length, precision, and moreupdatable = falseoncreatedAtprevents it from appearing in UPDATE statements- Spring Boot uses snake_case naming by default —
createdAtmaps tocreated_atautomatically @Transientexcludes 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.