Entities and Identifiers

This section documents Hibernate ORM 7.4.x (User Guide, Introduction, Query Language Guide, Data Repositories Guide), Jakarta Persistence 3.2, Hibernate Search 8.4.x, and the Hibernate Validator / Hibernate Reactive references — which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production.

Three older reference books were consulted as bibliography only while preparing these pages and are not the primary or main source for any page. All three predate Jakarta Persistence 3.2 and Hibernate ORM 6/7 (the javax.persistencejakarta.persistence namespace change, the ORM 6 query-engine rewrite, the Hibernate Search 6+ Elasticsearch backend), so the official documentation above wins on any discrepancy.

This section’s bibliography lists the reference material consulted while preparing these pages.

The identifier is the one mapping every entity must have. This page covers declaring an entity, choosing an access strategy, the identifier-generation strategies, composite keys, and the equals()/hashCode() question every entity eventually raises.

@Entity and @Table

@Entity
@Table(name = "book", schema = "library")
public class Book {

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

    private String title;

    // ...
}

@Entity is required; @Table is optional — omitted, Hibernate derives a table name from the entity name using its naming strategy. @Table also accepts uniqueConstraints and indexes, honored only by schema-generation tooling (see Schema Generation & Tooling), never enforced by Hibernate at runtime.

Field vs. property access

Hibernate decides access type by where @Id (or, for property access, the corresponding getter) is placed:

  • Field access — annotations on fields; Hibernate reads/writes fields directly (via reflection or bytecode enhancement), bypassing getters/setters entirely. Simpler, but any custom logic in a setter is skipped by Hibernate itself.

  • Property access — annotations on getters; Hibernate always goes through the accessor methods, so a computed or validated getter/setter runs on every read/write, including Hibernate’s own.

Mixing the two per-entity is allowed (@Access(AccessType.PROPERTY) on an individual field/getter) but adds a surprising edge case for every reader — pick one access type per entity and stay consistent.

@GeneratedValue strategies

Strategy Behavior

GenerationType.IDENTITY

Delegates to an auto-increment/identity column (SERIAL/IDENTITY). Simple, but the ID is known only after the INSERT executes, which disables JDBC batching for inserts of that entity (see Bulk Operations & Batching).

GenerationType.SEQUENCE

Uses a database sequence, optionally with @SequenceGenerator (allocationSize lets Hibernate pre-fetch a block of values in memory, avoiding a round-trip per insert). The generally preferred strategy on databases that support sequences (PostgreSQL, Oracle) because the ID is known before the insert, keeping batching intact.

GenerationType.TABLE

Emulates a sequence with a dedicated table row, for databases with neither identity columns nor sequences. Portable but the slowest option (extra SELECT/UPDATE per allocation) — rarely the right choice today.

GenerationType.UUID

Generates a random UUID client-side, no database round-trip. Good for distributed/sharded systems where a centrally-issued numeric key is a bottleneck; the trade-off is a wider key (16 bytes vs. 8) and worse index locality on inserts unless a time-ordered UUID scheme is used.

Custom (org.hibernate.id.IdentifierGenerator)

Implement the SPI directly (see Distributed ID Generation for a TSID/Snowflake-style example) and reference it via @GenericGenerator.

Composite keys

Two ways to model a multi-column primary key:

// (a) @EmbeddedId -- the key is a first-class @Embeddable value type
@Embeddable
public class OrderLineId implements Serializable {
    private Long orderId;
    private Integer lineNumber;
    // equals()/hashCode() over both fields, getters/setters omitted
}

@Entity
public class OrderLine {
    @EmbeddedId
    private OrderLineId id;
    private int quantity;
}

// (b) @IdClass -- the entity itself carries the plain @Id fields; the class named in
// @IdClass mirrors them and supplies equals()/hashCode()
@Entity
@IdClass(OrderLinePk.class)
public class OrderLine2 {
    @Id
    private Long orderId;
    @Id
    private Integer lineNumber;
}

@EmbeddedId is generally preferred: the key is one navigable object (orderLine.getId().getLineNumber()), and it composes naturally with @MapsId below. @IdClass avoids introducing a wrapper type when the composite fields are also meaningful as plain entity fields, at the cost of duplicating them (once as @Id fields, once in the @IdClass shadow class).

@NaturalId

@NaturalId marks a business key — unique, rarely-changing, but not the primary key (an ISBN, a username, a tax ID). It is not itself a generation strategy; it is a lookup optimization: Session.byNaturalId(Book.class) resolves an entity by its natural id through the same identity-map/first-level-cache machinery as find() by primary key, and, with @NaturalIdCache, through the second-level cache too (see Second-Level Cache).

@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NaturalId
    private String isbn;
}

// ...
Book book = session.byNaturalId(Book.class).using("isbn", "9781617290459").load();

Derived identity with @MapsId

@MapsId lets a @OneToOne (or @ManyToOne) child share its parent’s primary key value instead of having its own generated one — the classic "one row per parent, same PK" shared-primary-key pattern:

@Entity
public class Employee {
    @Id
    private Long id;
}

@Entity
public class EmployeeDetails {
    @Id
    private Long id; // same value as the owning Employee's id

    @MapsId
    @OneToOne
    @JoinColumn(name = "id")
    private Employee employee;
}

See Associations for the general @OneToOne shapes this composes with.

equals() and hashCode() for entities

The three plausible strategies, and why business-key equality is usually the right default:

  • Reference equality (no override) — correct only while both instances live in the same persistence context (the identity map already guarantees == there); breaks the moment an entity crosses contexts or ends up in a Set alongside a detached/newly-loaded duplicate.

  • Primary-key equality — compares the @Id value, but a transient entity has no id yet (null), so two distinct new entities can spuriously look "equal" (both null), and an entity’s identity/hash code changes the instant it gets an id assigned after persist() — a HashSet<Book> corrupts itself if the entity’s hash code changes while it is a member.

  • Business-key equality (recommended) — compare a stable, non-null attribute the domain considers the "real" identity (an @NaturalId, or a manually chosen field). This is what the User Guide recommends, and it is the only strategy stable across transient, managed, and detached states.

There is no single answer that fits every entity; an entity with no natural business key at all is one of the few legitimate cases for leaving equals()/hashCode() at their default Object behavior and simply avoiding putting transient instances into hash-based collections.