Entity Mapping & Annotations
@Entity, @Id, @GeneratedValue, @Column, @Table, and mapping relationships between entities.
@Entity and @Table
@Entity is the minimum required to make a class persistent; @Table is optional and only needed when the table name should differ from Hibernate's default (the class's simple name):
@Entity
@Table(name = "products", schema = "inventory")
public class Product {
// ...
}
Without @Table, Hibernate would map this class to a table literally named Product (or product, depending on the naming strategy configured) — explicit naming avoids surprises and keeps table names under your control as the schema evolves independently of class names.
@Id and @GeneratedValue
Every entity needs exactly one identifier field, marked @Id. @GeneratedValue delegates the actual value generation to a strategy:
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
| Strategy | How it works |
|---|---|
IDENTITY |
Delegates to the database's auto-increment column — simple, but Hibernate can't batch inserts efficiently since each row's ID isn't known until after the insert |
SEQUENCE |
Uses a database sequence object to pre-allocate IDs — supports batching, generally the preferred strategy on databases that support sequences (PostgreSQL, Oracle) |
AUTO |
Lets Hibernate pick a strategy appropriate for the configured database dialect |
TABLE |
Simulates a sequence using an ordinary table — portable but slower; rarely used today |
@Column
@Column fine-tunes how a field maps to its column — name, nullability, uniqueness, length, precision:
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "product_name", nullable = false, length = 200)
private String name;
@Column(nullable = false, unique = true)
private String sku;
@Column(precision = 10, scale = 2)
private BigDecimal price;
@Column(updatable = false)
private LocalDateTime createdAt;
}
A field with no @Column at all is still mapped — Hibernate infers a column name from the field name and reasonable defaults for everything else. @Column is for the cases where a default isn't good enough: enforcing NOT NULL/UNIQUE at the schema level, capping a VARCHAR length, or marking a column immutable after creation (updatable = false, useful for a createdAt timestamp).
Enums, dates, and other common field types
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@Column(nullable = false)
private LocalDateTime createdAt;
@Lob
private String notes;
}
@Enumerated(EnumType.STRING) stores the enum's name ("SHIPPED") rather than its ordinal position — always prefer STRING over the default ORDINAL, because reordering enum constants would otherwise silently corrupt already-stored data (ordinal 0 meaning something different after a new constant is inserted at the top of the enum). @Lob marks a field for large object storage (a CLOB/TEXT column, in this case).
Mapping relationships
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
}
@JoinColumn names the actual foreign key column on the owning side of the relationship (customer_id); mappedBy on the inverse side (here, on OrderItem's order field, referenced by name) tells Hibernate this side doesn't own the foreign key and shouldn't create one — it's purely a query-time reference. Relationship mapping in depth (fetch strategies, cascading, @ManyToMany) is covered together with Spring Data JPA's own relationship page, since both frameworks share the exact same JPA annotations.
Common mistakes
- Using
@Enumerated(EnumType.ORDINAL)(the JPA default when@Enumeratedis omitted or unqualified) — a reordered or inserted enum constant silently changes the meaning of every already-stored row. - Omitting
@Column(nullable = false)for fields that are genuinely required, relying only on Java-level null checks — the database itself should enforce the same constraint, since not every write necessarily goes through your application code (migrations, other services, manual fixes). - Not setting an explicit
@Tablename and being surprised by whatever naming strategy convention was configured (or defaulted) for the project. - Forgetting that
@JoinColumnbelongs on the owning side of a relationship, andmappedByon the inverse side — put them on the wrong sides and Hibernate either creates duplicate/unexpected columns or fails to map the relationship at all.