← Back to the section

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>.