skip to content

A field is annotated with Hibernate's @ColumnDefault("0") and the table was created by Hibernate's schema generation, yet new rows still arrive with NULL in that column. Why, and what actually makes the database default apply?

level: seniorimportance: should knowfreq 30%

answer

  1. Defaults apply only to omitted columns
  2. Static INSERT lists every mapped column
  3. @ColumnDefault touches DDL only
  4. @DynamicInsert omits nulls, fragments statement cache
  5. Read back with @Generated or just set it in Java

basics

~20 s

@ColumnDefault only adds DEFAULT to generated DDL. Hibernate still lists every mapped column in the INSERT, so it sends an explicit NULL and an explicit value always beats a column default. Use @DynamicInsert so null columns are omitted, plus @Generated to read the value back — or just set it in Java.

solid answer

~50 s

A column default only applies when the column is *absent* from the INSERT. Hibernate's default insert strategy is static: it builds one INSERT per entity at boot listing every insertable column, so a `null` field is sent as an explicit `null`, which the database stores instead of applying the default. `@ColumnDefault` never touches that statement — it exists purely to put `default 0` into DDL that Hibernate generates. Three ways out: 1. **Set it in Java** — a field initialiser. Simplest, keeps the entity and row in agreement, no extra SQL. 2. **`@DynamicInsert`** on the entity — Hibernate builds the INSERT per flush and omits null-valued columns, so the default applies; add `@Generated(event = EventType.INSERT)` if the entity must see the applied value, which costs a read-back. 3. **Make the database enforce it** — `not null default 0` in the migration, and stop mapping the column as writable. Also note `@ColumnDefault` is irrelevant when the schema comes from migrations rather than hbm2ddl.

code

java · 17 lines
java
// (1) application owns it
@ColumnDefault("0")
private Integer retryCount = 0;

// (2) let the database default apply, then read it back
@Entity
@DynamicInsert
public class Job {
    @ColumnDefault("0")
    @Generated(event = EventType.INSERT)
    private Integer retryCount;
}

// (3) database is the sole authority
@Column(insertable = false, updatable = false)
@Generated(event = EventType.INSERT)
private Integer retryCount;

go deeper

for a junior

Say that a database default only fills a column the INSERT leaves out, and Hibernate's INSERT includes every mapped column.

for a middle

Explain the static-versus-dynamic insert distinction and that @ColumnDefault only affects generated DDL.

for a senior

Weigh the fixes: Java initialiser versus @DynamicInsert (statement-cache and batching cost) versus database-owned column with @Generated read-back; note that production schemas come from migrations.

for a principal

Decide where the invariant lives: if non-ORM writers exist, the default belongs in the schema and the mapping must stop writing the column; otherwise keep one static INSERT and set the value in code.

## The symptom The entity says `@ColumnDefault("0") private Integer retryCount;`, the generated DDL says `retry_count integer default 0`, and yet `select * from job` shows nulls. Nothing is broken; two independent mechanisms are being confused. ## Why the default does not fire SQL column defaults are a *fallback for omitted columns*. `insert into job (id, retry_count) values (?, null)` supplies the column explicitly, so the engine stores the supplied value — `null` — and the default is never consulted. Only `insert into job (id) values (?)` lets the default apply. Hibernate, by default, uses **static insert SQL**: at boot it builds one INSERT string per entity listing every insertable column, and reuses it for every row. This is a deliberate performance choice — one prepared statement per entity means high statement-cache hit rates and lets rows batch together, because JDBC batching requires the *same* SQL text. The price is that Hibernate cannot omit a column just because it happens to be null in this instance. `@ColumnDefault` does not participate in that at all. It is a schema-generation annotation: it appends `default <literal>` to the column in DDL emitted by `hbm2ddl`/the schema tool. If your schema comes from Liquibase or Flyway (as it should in production), `@ColumnDefault` is inert documentation. ## The fixes, and their costs **1. Initialise the field in Java.** `private Integer retryCount = 0;` — the INSERT now carries 0, the row and the entity agree, no extra SQL, no annotations. This is the right answer most of the time, because the value is application knowledge anyway. Keep the DDL default too, as a guard for writers that are not Hibernate. **2. `@DynamicInsert`.** On the entity, this switches Hibernate to building the INSERT at flush time from the columns that actually have values, omitting nulls — so the database default applies. Costs: a different SQL string per combination of non-null columns, which fragments the statement cache and can prevent rows from batching together, plus the CPU of building SQL per insert. Also, the entity still holds `null` afterwards unless you additionally mark the property `@Generated(event = EventType.INSERT)`, which triggers a read-back (a returning clause or a follow-up SELECT per row). `@DynamicUpdate` is the analogous switch for UPDATE, and it exists for a different reason — narrowing update statements to changed columns. **3. Push it fully into the database.** Declare `not null default 0` in the migration and mark the Java property `insertable = false, updatable = false` with `@Generated(event = EventType.INSERT)`. Now the database is the single authority — every writer, including migrations and other services, gets the default — and Hibernate reads the value back. Best when the invariant must hold for non-Hibernate writers; costs the read-back round trip. ## Related traps in the same family - **Nullability.** `@Column(nullable = false)` also only affects generated DDL (and Hibernate's own pre-insert check when bean validation is wired). It does not add a constraint to a table created by migrations. - **Partial updates.** Without `@DynamicUpdate`, Hibernate's UPDATE lists all columns, so a stale field in a detached entity that you `merge()` will overwrite whatever the database has. That is the same "Hibernate writes the whole row" behaviour biting from the other side. - **Defaults that are expressions.** `@ColumnDefault("now()")` still only reaches DDL; if you want the value in the entity, it is a `@Generated` column or a Java-side timestamp generator. ## How to answer in an interview Name the mechanism precisely: defaults apply to omitted columns; Hibernate's static INSERT never omits a mapped column; `@ColumnDefault` is DDL-only. Then give the three options with their trade-offs, and say which you would pick — usually the Java initialiser for application-owned values, and the database default plus `@Generated` for values that every writer must obey.

  • What does @DynamicInsert cost, and when would you avoid it?
    Hibernate builds the INSERT text per flush from the non-null columns, so a table with many nullable columns produces many distinct SQL strings. That reduces prepared-statement cache hits on both the driver and the database, and it prevents rows with different null patterns from joining the same JDBC batch. On a high-volume insert path, prefer setting the value in Java and keeping one static INSERT.
  • Does @Column(nullable = false) enforce anything at runtime?
    Only indirectly. It emits `not null` into DDL generated by Hibernate's schema tool, and it lets Hibernate reject a null before hitting the database in some paths. If the schema comes from migrations, the annotation is documentation and the real enforcement is the constraint in the migration script.

saying these in an interview costs you the question

  • Thinking @ColumnDefault changes the INSERT statement
  • Assuming a column default overrides an explicitly supplied NULL
  • Adding @DynamicInsert on hot insert paths without considering batching and statement caching
  • Expecting the entity to see the applied default without any read-back
  • Relying on hbm2ddl-generated DDL as the source of truth in production

context