Hibernate & Entity Mapping

Mapping an Entity

18 min Lesson 3 of 13

Mapping an Entity

In JPA and Hibernate, a plain Java class becomes a persistent entity the moment you annotate it correctly. Three annotations do almost all of the work: @Entity tells the persistence provider that the class participates in ORM, @Table controls how that class maps to a database table, and @Column fine-tunes how each field maps to a column. This lesson covers all three in depth — the defaults, the options, and the trade-offs that matter in production code.

@Entity — Registering a Class with JPA

Placing @Entity on a class is the minimal declaration that a class is managed by the persistence context. Hibernate will include it in schema generation, scanning, and JPQL queries.

import jakarta.persistence.Entity; import jakarta.persistence.Id; @Entity public class Product { @Id private Long id; private String name; private int stock; // JPA requires a public or protected no-arg constructor protected Product() {} public Product(Long id, String name, int stock) { this.id = id; this.name = name; this.stock = stock; } // getters / setters omitted for brevity }

By default, JPA maps this class to a table whose name equals the unqualified class name — Product maps to the table Product (or product on case-insensitive engines like MySQL). This is fine for quick prototyping, but in a team or enterprise environment you almost always want explicit control.

The no-arg constructor is mandatory. JPA instantiates entities via reflection when loading rows from the database, and it needs a no-arg constructor to do so. Make it protected rather than public to signal that application code should not call it directly — the persistence provider can still reach it.

@Table — Controlling the Table Name and Constraints

@Table sits at the class level alongside @Entity. Its most important attribute is name, which sets the exact SQL table name.

import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; import jakarta.persistence.Index; @Entity @Table( name = "products", schema = "catalog", uniqueConstraints = { @UniqueConstraint(name = "uq_product_sku", columnNames = {"sku"}) }, indexes = { @Index(name = "idx_product_category", columnList = "category_id") } ) public class Product { @Id private Long id; private String sku; private String name; @Column(name = "category_id") private Long categoryId; protected Product() {} }

The key attributes of @Table are:

  • name — the exact table name in SQL. Always set this explicitly in production code; do not rely on the class-name default.
  • schema — the database schema or namespace. Useful in multi-schema deployments (e.g., catalog, orders, auth as separate schemas in the same database).
  • catalog — the database catalog name (less commonly needed; relevant for certain databases like SQL Server).
  • uniqueConstraints — DDL-level unique constraints generated by Hibernate's schema tooling. These are purely declarative hints for schema generation (spring.jpa.hibernate.ddl-auto=create or validate) — they do not enforce anything at the JPA level at runtime.
  • indexes — additional indexes to emit during schema generation. Again, DDL-only; Hibernate does not use these at query time.
Lowercase, plural, snake_case table names. Most Java naming conventions use PascalCase class names, but most SQL naming conventions use snake_case table names. Always declare @Table(name = "orders") explicitly rather than leaving it to chance. This also prevents surprises when moving between a case-sensitive filesystem (Linux) and a case-insensitive one (macOS, Windows).

@Column — Mapping Fields to Columns

Without any annotation, JPA maps each non-static, non-transient field to a column whose name matches the field name. @Column lets you override every aspect of that mapping.

import jakarta.persistence.*; import java.math.BigDecimal; import java.time.LocalDateTime; @Entity @Table(name = "products") public class Product { @Id private Long id; @Column(name = "sku", nullable = false, unique = true, length = 30) private String sku; @Column(name = "display_name", nullable = false, length = 200) private String name; @Column(name = "unit_price", nullable = false, precision = 10, scale = 2) private BigDecimal unitPrice; @Column(name = "stock_quantity", nullable = false, columnDefinition = "INT DEFAULT 0") private int stockQuantity; @Column(name = "description", columnDefinition = "TEXT") private String description; @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; @Column(name = "updated_at") private LocalDateTime updatedAt; protected Product() {} }

The most important @Column attributes:

  • name — the SQL column name. Follow the same snake_case convention as table names.
  • nullable — when false, Hibernate adds a NOT NULL constraint during schema generation. It also enables Bean Validation integration when the jakarta.validation API is on the classpath.
  • unique — adds a single-column unique constraint. For multi-column unique constraints use @Table(uniqueConstraints = ...) instead.
  • length — applies to VARCHAR columns (String fields). Defaults to 255 — always set an explicit length for string columns that are indexed or whose max length you know.
  • precision and scale — used for DECIMAL/NUMERIC columns (BigDecimal fields). precision = total significant digits; scale = digits after the decimal point.
  • updatable — when false, Hibernate omits this column from SQL UPDATE statements. Use it for audit fields like created_at that must never change after insert.
  • insertable — when false, Hibernate omits the column from SQL INSERT statements. Useful for database-generated columns (e.g., a computed column or a default supplied by a trigger).
  • columnDefinition — a raw DDL fragment that overrides the entire column definition in schema generation. Use sparingly; it ties your code to a specific database dialect.
nullable = false is a DDL hint, not a runtime guard. Setting nullable = false tells Hibernate to emit NOT NULL when generating the schema and to enable Bean Validation constraints, but it does not throw a JPA exception if you persist an entity with a null value. The database will throw a constraint-violation error on flush — which surfaces as a ConstraintViolationException wrapping a DataIntegrityViolationException in Spring. Always pair it with @NotNull from Bean Validation (jakarta.validation.constraints.NotNull) to catch the problem at the service layer before hitting the database.

Putting It All Together: a Realistic Entity

Here is a complete, production-quality entity that uses all three annotations coherently:

package com.example.catalog.domain; import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import java.math.BigDecimal; import java.time.LocalDateTime; @Entity @Table( name = "products", uniqueConstraints = @UniqueConstraint(name = "uq_products_sku", columnNames = "sku"), indexes = @Index(name = "idx_products_category", columnList = "category_id") ) public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank @Column(name = "sku", nullable = false, unique = true, length = 30) private String sku; @NotBlank @Column(name = "display_name", nullable = false, length = 200) private String name; @NotNull @Positive @Column(name = "unit_price", nullable = false, precision = 10, scale = 2) private BigDecimal unitPrice; @Column(name = "category_id") private Long categoryId; @Column(name = "description", columnDefinition = "TEXT") private String description; @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; @Column(name = "updated_at") private LocalDateTime updatedAt; protected Product() {} public Product(String sku, String name, BigDecimal unitPrice) { this.sku = sku; this.name = name; this.unitPrice = unitPrice; this.createdAt = LocalDateTime.now(); } // getters and setters ... }

Access Type: Field vs. Property

JPA determines where to read and write values based on where you place the @Id annotation. If @Id is on a field, Hibernate accesses all fields directly via reflection (field access). If @Id is on a getter method, Hibernate accesses all persistent state through getters and setters (property access).

Field access is the modern default and strongly preferred. It keeps mapping annotations co-located with the state they describe, avoids the need for artificial getter/setter boilerplate purely for JPA, and works cleanly with records in future versions. Never mix field and property access in the same entity hierarchy without an explicit @Access annotation — the behaviour is undefined.

Summary

Three annotations form the skeleton of every JPA entity. @Entity registers the class with the persistence provider. @Table controls the SQL table name, schema, unique constraints, and indexes. @Column fine-tunes each field's column name, nullability, length, precision, and mutability. Always be explicit — relying on naming defaults leads to surprises as the codebase grows or the database dialect changes. Pair nullable = false with Bean Validation annotations so violations are caught before they reach the database. In the next lesson you will see how @Id and @GeneratedValue control primary-key generation strategies.