← Back to the section

Hibernate is an ORM library: it takes a Java object and translates it into a table row (and back) on its own. For that to work, you need to describe how exactly the fields of a class correspond to columns. This process is called entity mapping.

What an entity is

An entity is an ordinary Java class that Hibernate knows how to save to a database and load from it. The minimal requirements: 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 — the behavior depends on the hibernate.physical_naming_strategy setting, so an explicit @Table is more reliable.

Primary key and identifier generation

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

Generation strategies:

StrategyHow it works
IDENTITYRelies on AUTO_INCREMENT / GENERATED ALWAYS AS IDENTITY in the database. Hibernate inserts the row and 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 — this blocks batching (JDBC batch). With the SEQUENCE strategy the ID is requested ahead of time via nextval, so several INSERTs can be sent in a single batch, which sharply reduces the number of round-trips to the database.

@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 in one call to the sequence and consumes it incrementally. Under heavy inserts this saves database round-trips.

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

Columns: @Column

By default Hibernate maps each field to a column with the same name (accounting for the naming strategy). @Column lets you set 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) and signals Bean Validation.
  • length — the 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 example, for columns managed by triggers.

Enums: the @Enumerated pitfall

An enum can be stored in two ways:

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

@Enumerated(EnumType.ORDINAL) // DANGEROUS
private Priority priority;

EnumType.ORDINAL stores the constant's ordinal position (0, 1, 2…). Add a new constant in the middle of the enum or reorder the existing ones — and all the old data in the database becomes wrong. This is a silent bug that is very hard to detect.

Rule: always use EnumType.STRING. The string value is readable, resistant to refactoring, and clear when you inspect the data in a database console.

Embeddable objects: @Embedded and @Embeddable

Sometimes several columns of one table logically form a 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;
}
@Entity
@Table(name = "customers")
public class Customer {

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

    @Embedded
    private Address address;
}

Hibernate stores city, street, postal_code right in the customers table — there is no extra table. Meanwhile, in the Java code the address is a standalone object with its own validation logic.

If a single @Embeddable type is used twice in an entity (for example, deliveryAddress and billingAddress), the column names have to 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 you need a field in the Java class but don't want to persist it to the database — add @Transient:

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

Without @Transient, Hibernate will try to find a matching column and throw an error on a schema mismatch (or silently write null — which is worse).

Basic types and conversions

Hibernate can map the standard Java types directly: String, Integer, Long, BigDecimal, Boolean, LocalDate, LocalDateTime, ZonedDateTime, UUID. For Hibernate 6 / Spring Boot 3, support for the Java Time API is built in.

For non-standard types (for example, storing a list of strings as a JSON column) you use @Convert with an AttributeConverter<X, Y> implementation:

@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; ORDINAL 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>.
  • Associations between entities — @OneToMany, @ManyToOne, @ManyToMany, cascades.
  • Persistence Context — how Hibernate tracks changes and when it flushes.
  • Inheritance strategies — SINGLE_TABLE, JOINED, TABLE_PER_CLASS.
  • Spring Data JPA — repositories on top of Hibernate: JpaRepository, @Query, projections.