← Back to the section

HIBERNATE Phase 11. Entity mapping

Entity Mapping in Hibernate and JPA

Java

Ids, columns, enums, embedded values, transient fields, keys and migrations.

1. An entity starts with a class and a primary key

1 / 5

@Entity declares an entity and @Id its identifier. Generation can be identity, sequence, table or UUID.

  • A no-argument constructor is required.
  • The identifier stays stable for the row's lifetime.
  • Composite keys use EmbeddedId or IdClass.
@Entity Order @Id sequence UUID

2. @Column controls names and constraints

2 / 5

The annotation defines the column name, nullability, length and uniqueness. The database remains the source of constraint truth.

  • Field and column names may differ.
  • nullable=false marks a required value.
  • Date and money types are chosen explicitly.
totalAmount total_amount numeric(19,2) not null

3. Enums are stored by name, not by position

3 / 5

@Enumerated(EnumType.STRING) stores the constant name. The ordinal changes when constants are reordered.

  • Strings survive order refactoring.
  • Renaming a constant requires migration.
  • An external contract may need a separate code.
NEW "NEW" ordinal: 0 - 1 - depends on order

4. Embedded values live inside the entity table

4 / 5

@Embeddable defines a value and @Embedded embeds it. @Transient excludes computed fields.

  • Address lives inside orders.
  • The value object has no id of its own.
  • A Java computation need not be a column.
Order @Embedded Address @Transient fullAddress - not a column

5. Schema changes belong in migrations, not ddl-auto

5 / 5

ddl-auto=validate only checks the mapping. Schema changes go through Flyway or Liquibase.

  • JSONB and arrays need special mapping.
  • Timestamptz stores a moment with a zone.
  • Audit and soft delete are application and database rules.
V1 - V2 migration validate - check, do not change

Hibernate is an ORM library: it takes a Java object and turns it into a table row, and back. For that to work, you have to describe how the fields of a class correspond to columns. That is what entity mapping means.

object fields spread across the columns of one row class Customer row in customers @Id @Column @Enumerated @Embedded Long id String firstName Status status Address address id first_name status = 'ACTIVE' city street postal_code 1 2 3 4 5 String displayLabel Long idid String firstNamefirst_name Status statusstatus = 'ACTIVE' Address addresscitystreetpostal_code @Transient — no column

One table row is assembled out of the object's fields. The column name comes from @Column or from the naming strategy; @Enumerated(STRING) puts the constant's name into the column; an embedded @Embedded object expands into several columns of the same row, while a field marked @Transient never reaches the database at all.

What an entity is

An entity is an ordinary Java class that Hibernate can save to a database and load back. The minimum: the @Entity annotation, a no-argument constructor (it may be protected), and a field with @Id.

import jakarta.persistence.*;

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

    @Id
    private Long id;

    private String name;
}

@Table(name = "products") sets the table name explicitly. Without it, Hibernate uses the class name — behavior that depends on hibernate.physical_naming_strategy, so an explicit @Table is more reliable.

Primary key and identifier generation

Every entity must have an @Id. To have Hibernate generate the value, add @GeneratedValue.

Generation strategies:

StrategyHow it works
IDENTITYRelies on AUTO_INCREMENT / GENERATED ALWAYS AS IDENTITY in the database. Hibernate inserts the row, then reads back the generated key.
SEQUENCEUses a sequence object in the database. Hibernate requests the next ID in advance.
AUTOHibernate picks the strategy itself — usually SEQUENCE for PostgreSQL.

Why is SEQUENCE preferable to IDENTITY? With IDENTITY, Hibernate doesn't know the ID until the INSERT runs, and that blocks batching (JDBC batch). With SEQUENCE the ID is requested ahead of time via nextval, so several INSERTs go to the database in one batch.

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "product_seq")
@SequenceGenerator(name = "product_seq", sequenceName = "product_id_seq", allocationSize = 50)
private Long id;

allocationSize = 50 means Hibernate reserves a block of 50 values per sequence call and hands them out one by one — fewer database calls under heavy inserts.

If the identifier is a UUID, see UUID in PostgreSQL — the uuid type and generation at the database level.

Columns: @Column

By default Hibernate maps each field to a column of the same name (per the naming strategy). @Column sets the parameters explicitly:

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

@Column(name = "price", precision = 10, scale = 2)
private BigDecimal price;

@Column(name = "in_stock", columnDefinition = "boolean default true")
private boolean inStock;

Important attributes:

  • nullable = false — adds NOT NULL to the DDL if Hibernate generates the schema. Bean Validation is separate: an empty field is caught by @NotNull, not by @Column. The link runs the other way — seeing @NotNull, Hibernate puts NOT NULL into the generated schema.
  • length — maximum length for VARCHAR (255 by default).
  • precision / scale — precision for NUMERIC.
  • insertable = false / updatable = false — Hibernate leaves the field out of INSERT / UPDATE. Used for columns managed by triggers.

Enums: the @Enumerated pitfall

Hibernate stores an enum one of two ways: by the constant's ordinal position or by its name. @Enumerated makes the choice; the default — no annotation, or one without a parameter — is ORDINAL, the number.

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Status status;

Why the number is dangerous shows up on a model: the database holds a number, its meaning lives in the Java code. Add a constant in the middle — and old rows read back wrong, silently.

live example

public class EnumStorageDemo {

    enum StatusV1 { NEW, PAID, SHIPPED }

    enum StatusV2 { NEW, ON_HOLD, PAID, SHIPPED }

    public static void main(String[] args) {
        StatusV1 saved = StatusV1.PAID;
        int ordinalColumn = saved.ordinal();
        String stringColumn = saved.name();
        System.out.println("stored " + saved + ": ORDINAL=" + ordinalColumn + ", STRING=" + stringColumn);
        System.out.println("ON_HOLD added second, reading the same rows back:");
        System.out.println("  ORDINAL -> " + StatusV2.values()[ordinalColumn]);
        System.out.println("  STRING  -> " + StatusV2.valueOf(stringColumn));
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

A paid order has turned into a held one, and nothing reported an error. With a string that never happens: the column holds the constant's name, and the meaning is visible in the database.

live example

SELECT status, count(*) AS orders FROM orders GROUP BY status ORDER BY status;
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Rule: EnumType.STRING — always.

Embeddable objects: @Embedded and @Embeddable

Sometimes several columns of one table form a logical group — an address, for example. Instead of piling everything into a flat entity class, you can extract an embeddable object (@Embeddable):

@Embeddable
public class Address {
    private String city;
    private String street;

    @Column(name = "postal_code", length = 10)
    private String postalCode;
}

In the Customer entity the field is marked @Embedded:

@Embedded
private Address address;

Hibernate stores city, street, postal_code in the customers table itself — no extra table. In the Java code the address stays a standalone object with its own validation logic.

If one @Embeddable type is used twice in an entity (deliveryAddress and billingAddress), the column names must be overridden via @AttributeOverrides:

@Embedded
@AttributeOverrides({
    @AttributeOverride(name = "city", column = @Column(name = "billing_city")),
    @AttributeOverride(name = "street", column = @Column(name = "billing_street")),
    @AttributeOverride(name = "postalCode", column = @Column(name = "billing_postal_code"))
})
private Address billingAddress;

Fields without mapping: @Transient

If a field is needed in the Java class but not in the database — add @Transient:

@Transient
private String displayLabel; // computed on the fly, not stored

Without @Transient, Hibernate looks for a column for the field. Finding none, the application fails — at startup when schema validation is on, or on the first query against the table. A similarly named column is worse: the value quietly goes into it, no error, and the database collects junk.

Basic types and conversions

Hibernate maps the standard Java types directly: String, Integer, Long, BigDecimal, Boolean, LocalDate, LocalDateTime, ZonedDateTime, UUID. In Hibernate 6 / Spring Boot 3 the Java Time API is supported out of the box.

For non-standard types there is @Convert with an AttributeConverter<X, Y>: it describes how a value turns into a column and back. Below, a list of tags is folded into one comma-separated text column:

@Converter(autoApply = false)
public class StringListConverter implements AttributeConverter<List<String>, String> {

    @Override
    public String convertToDatabaseColumn(List<String> list) {
        return list == null ? null : String.join(",", list);
    }

    @Override
    public List<String> convertToEntityAttribute(String value) {
        return value == null ? List.of() : List.of(value.split(","));
    }
}
@Convert(converter = StringListConverter.class)
@Column(name = "tags")
private List<String> tags;

In short

  • @Entity + a no-argument constructor + @Id — the minimum for an entity.
  • SEQUENCE beats IDENTITY under high load: it enables JDBC batching.
  • @Enumerated(EnumType.STRING) — always; the default is ORDINAL, and it breaks when the order of constants changes.
  • @Embedded / @Embeddable — group columns into an object without a new table.
  • @Transient — a field in memory, not in the database.
  • For non-standard types — AttributeConverter<X, Y>.