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:
| Strategy | How it works |
|---|---|
IDENTITY | Relies on AUTO_INCREMENT / GENERATED ALWAYS AS IDENTITY in the database. Hibernate inserts the row and then reads back the generated key. |
SEQUENCE | Uses a sequence object in the database. Hibernate requests the next ID in advance. |
AUTO | Hibernate 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— addsNOT NULLto the DDL (if Hibernate generates the schema) and signals Bean Validation.length— the maximum length forVARCHAR(255 by default).precision/scale— precision forNUMERIC.insertable = false/updatable = false— Hibernate leaves the field out ofINSERT/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.SEQUENCEbeatsIDENTITYunder high load: it enables JDBC batching.@Enumerated(EnumType.STRING)— always;ORDINALbreaks 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>.
What to read next
- 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.