skip to content

A column is populated by the database itself — a DEFAULT, a trigger, or a computed column. How do you make Hibernate load that value back into the in-memory entity after an INSERT or UPDATE, and what does it cost in SQL?

level: middleimportance: should knowfreq 40%

answer

  1. @Generated(event = INSERT/UPDATE)
  2. Column dropped from the write SQL
  3. Read-back: RETURNING or extra SELECT
  4. Extra select per row hurts batching
  5. @ColumnDefault is DDL only

basics

~20 s

Mark the property with Hibernate's @Generated, naming the events (INSERT, UPDATE) at which the database produces it. Hibernate then omits the column from the write statement and reads the value back — with an extra SELECT, or via INSERT ... RETURNING on dialects that support it.

solid answer

~50 s

By default Hibernate assumes it owns every mapped column: it writes what is in memory and never re-reads the row, so a trigger- or default-produced value stays invisible until you `refresh()`. `@org.hibernate.annotations.Generated(event = {EventType.INSERT, EventType.UPDATE})` changes that contract. Hibernate excludes the column from the INSERT (and/or UPDATE), lets the database produce it, then immediately reads it back into the entity so the managed instance matches the row. The read-back is a real round trip. On dialects that support returning generated values in the write statement (PostgreSQL, Oracle) Hibernate can use `insert ... returning`; otherwise it issues a separate `select` by primary key straight after the write. Either way it is per row, and a separate SELECT undermines JDBC batching — with a batch of inserts Hibernate must fetch the values again. Related: `@GeneratedColumn` also emits the expression into generated DDL; `@ColumnDefault` only affects DDL, not the write path.

code

java · 18 lines
java
@Entity
public class Invoice {
    @Id @GeneratedValue
    private Long id;

    // filled by a BEFORE INSERT trigger
    @Generated(event = EventType.INSERT)
    @Column(insertable = false, updatable = false)
    private String reference;

    // maintained by a trigger on every write
    @Generated(event = {EventType.INSERT, EventType.UPDATE})
    private Instant lastTouchedAt;

    // stored computed column, emitted into generated DDL
    @GeneratedColumn("net_amount + tax_amount")
    private BigDecimal grossAmount;
}

go deeper

for a junior

Know that Hibernate does not re-read rows after writing, so database-generated values need @Generated (or a refresh) to show up.

for a middle

Name the annotation and its events, explain that the column is dropped from the INSERT/UPDATE and read back afterwards.

for a senior

Discuss the read-back mechanism per dialect, the batching cost, and the discipline of deciding whether the application or the database owns each column.

for a principal

Frame it as a contract question: which invariants belong in the database because every writer must obey them, and what round-trip budget that buys you on hot write paths.

## The default contract Hibernate's write path is built on a snapshot: when an entity becomes managed, Hibernate stores the loaded state; at flush it compares the current state with the snapshot and writes what changed. Nothing in that loop re-reads the row. That is deliberate — an extra SELECT after every write would double the round trips. The consequence is that anything the database decides for itself is invisible to the entity. A `DEFAULT now()`, a `BEFORE INSERT` trigger that stamps an audit column, a PostgreSQL `GENERATED ALWAYS AS (...) STORED` column, a computed column in SQL Server — all of them produce a value in the row that the managed entity does not have. The field stays `null` (or whatever Java put there) for the rest of the persistence context, and if you then flush an unrelated change, Hibernate may write your stale `null` back over the database's value. ## Telling Hibernate the database owns the column `@Generated` is the mapping-level statement "the database produces this; do not write it, read it". In Hibernate 6 it takes an `event` attribute: - `event = EventType.INSERT` — produced on insert only (creation timestamps, defaults, identity-like columns). - `event = {EventType.INSERT, EventType.UPDATE}` — produced on every write (audit columns maintained by triggers, stored computed columns). With `@Generated` in place Hibernate does three things: it leaves the column out of the generated INSERT/UPDATE column list, it flags the property as needing a read-back after that statement, and it treats the property as non-dirtyable so it never triggers an UPDATE on its own. In Hibernate 5 the same idea was expressed as `@Generated(GenerationTime.INSERT | ALWAYS)`; `GenerationTime` was replaced by `EventType` in Hibernate 6, and `@Generated` also gained a `writable` flag for the case where the application supplies a value but the database may override it. ## How the value gets back Two mechanisms, chosen by the dialect: 1. **Returning clause.** Where the database can return values from the write statement — PostgreSQL's `insert ... returning`, Oracle's `returning ... into` — Hibernate folds the read into the write. One round trip, no extra statement. 2. **Follow-up SELECT.** Otherwise Hibernate issues `select <generated columns> from <table> where <id> = ?` immediately after the INSERT or UPDATE, in the same transaction, and copies the values into the entity and its snapshot. The second form is the expensive one. It is per entity, not per flush, so inserting 1,000 rows means 1,000 extra selects. It also interacts badly with JDBC batching: Hibernate cannot read back values for a whole batch in one statement on most dialects, so heavy generated-column use on a bulk-insert path is a measurable regression. If you are inserting in bulk and do not need the value immediately, it is often better to leave the column unmapped or mapped plainly, and reload on demand. ## Neighbouring annotations - `@GeneratedColumn("unit_price * quantity")` — Hibernate emits the column as a database-computed column in generated DDL *and* treats it as generated for read-back. This is the schema-side twin of `@Formula`: computed at write time, stored, indexable. - `@ColumnDefault("0")` — pure DDL sugar. It puts `default 0` in generated DDL and changes nothing about the INSERT, so it does not by itself make the default apply. - `@CurrentTimestamp` / `@CreationTimestamp` / `@UpdateTimestamp` — value generation, but normally performed in the JVM rather than by the database. ## Practical guidance Decide who owns each column and be explicit. If the database owns it (trigger, stored computed column, default that must win), map it `@Generated` and accept the read-back; ideally also make the field non-settable in Java so nobody assumes they can change it. If the application owns it, set it in code and let Hibernate write it — that is one round trip and no surprises. The failure mode to avoid is a column that both sides believe they own: the entity writes `null`, the trigger overwrites it, and the two disagree until the next reload.

  • How does @Generated interact with JDBC insert batching?
    Badly, on dialects that need a follow-up SELECT: Hibernate must read each row back after the write, so the per-row select traffic returns even though the inserts themselves batched. On PostgreSQL or Oracle, where the value can come back from the write statement, the impact is far smaller. If a bulk-load path does not need the generated value in memory, avoid mapping it as generated.
  • What is the difference between @Generated and @Formula?
    @Formula has no column: the expression is evaluated in the SELECT every time the entity is loaded. @Generated names a real column whose value the database produces at write time; Hibernate skips writing it and reads it back. @Formula costs on every read, @Generated costs one read-back per write and can be indexed because the value is stored.

saying these in an interview costs you the question

  • Expecting a trigger-populated column to appear in the entity without any mapping hint
  • Mapping the column normally and letting Hibernate write null over the trigger's value
  • Thinking @ColumnDefault makes the database default apply to Hibernate's inserts
  • Assuming the read-back is free rather than an extra round trip per row
  • Using Hibernate 5's GenerationTime API on Hibernate 6 and expecting it to compile

context