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.
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:
| Strategy | How it works |
|---|---|
IDENTITY | Relies on AUTO_INCREMENT / GENERATED ALWAYS AS IDENTITY in the database. Hibernate inserts the row, 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, 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— addsNOT NULLto 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 putsNOT NULLinto the generated schema.length— maximum length forVARCHAR(255 by default).precision/scale— precision forNUMERIC.insertable = false/updatable = false— Hibernate leaves the field out ofINSERT/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.SEQUENCEbeatsIDENTITYunder high load: it enables JDBC batching.@Enumerated(EnumType.STRING)— always; the default isORDINAL, 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>.
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.