spring.jpa.hibernate.ddl-auto

Controls whether Hibernate creates, updates, validates or leaves alone the database schema at startup; use validate or none in production.

Signature
spring.jpa.hibernate.ddl-auto=validate
  • Added in Spring Boot Spring Boot 4.1 (verified present)

Parameters

ParameterTypeDescription
$noneoptionalvalue= none (non-embedded database)Do nothing. The default for a non-embedded database, and the default whenever Flyway or Liquibase manages the schema.
$validateoptionalvalueCompare entities with the existing tables and fail startup on a mismatch. Makes no changes.
$updateoptionalvalueAdd missing tables and columns. Never drops or alters existing ones.
$createoptionalvalueDrop and recreate the schema at startup, destroying existing data.
$create-dropoptionalvalue= create-drop (embedded database)Create at startup and drop when the application shuts down. The default for an embedded database with no schema manager.
$create-onlyoptionalvalueCreate the schema without dropping anything first.
$dropoptionalvalueDrop the schema at startup.
$truncateoptionalvalueDelete the data in the mapped tables at startup, keeping the schema.

Return value

n/aWhat Hibernate does to the schema when the EntityManagerFactory starts. It is a shortcut for Hibernate's hibernate.hbm2ddl.auto setting.

Errors

With validate, a missing table or column, or a column of the wrong type, fails startup with a SchemaManagementException that names it.

Hibernate can generate DDL from your entity classes. spring.jpa.hibernate.ddl-auto decides what it does with that ability when the application starts: nothing, check the database against the entities, add what is missing, or throw the schema away and build it again.

The default depends on the database

Boot picks create-drop for an embedded database such as H2 when no schema manager (Flyway or Liquibase) is present, and none for everything else. That is why a project with H2 seems to make tables by magic and then appears to break the day it moves to PostgreSQL: nothing creates the tables any more.

Which value for which environment

ValueUse it for
create-dropThrowaway databases in tests and demos
updateA personal scratch database while modelling, never shared ones
validateAny environment where Flyway owns the schema: fail fast on drift
noneProduction, when you do not want Hibernate touching the schema at all

Why update is not a migration tool

update only adds. It will not rename a column (it adds a new, empty one), will not drop columns you removed from the entity, will not change a type, and does not record what it did. Two instances starting at once can both try to alter the same table. On a production database the result is a schema nobody can reproduce, and a renamed field silently loses its data. Version the schema with Flyway instead and let each migration run once, in order, in a transaction where the database supports it.

Validate is cheap insurance

With Flyway in charge, validate turns a mismatch between a migration and an entity into a startup failure with a clear message, rather than an SQL error on the first request that touches the column. It does not catch everything (indexes and most constraints are not compared), but it catches the common mistakes: a forgotten migration, a misspelt column, a varchar where the entity expects a number.

Do not combine create or create-drop with a data.sql file against a shared database. One accidental profile switch wipes real data.

Example

Example · properties
# Flyway owns the schema; Hibernate only checks that the entities match it.
spring.flyway.enabled=true
spring.jpa.hibernate.ddl-auto=validate

# Leave show-sql off; to see the SQL locally use logging.level.sql=DEBUG instead.
spring.jpa.show-sql=false

When to use it

  • Failing a deployment early when an entity change shipped without its Flyway migration
  • Letting repository tests build a fresh schema in an embedded database on every run
  • Sketching a new data model on a personal database before writing the first migration

Frequently asked questions

What is the default value of spring.jpa.hibernate.ddl-auto?

create-drop when the database is embedded (H2, HSQLDB, Derby) and no Flyway or Liquibase is present; none in every other case.

Is ddl-auto=update safe in production?

No. It cannot rename or drop columns or change types, records nothing, and can race when several instances start together. Use Flyway or Liquibase migrations and set ddl-auto to validate or none.

Why did my tables disappear when the application stopped?

The effective value was create-drop, which drops the schema on shutdown. That happens by default with an embedded database, or when the value was copied from a test profile.

More examples

An entity and the table it expects

With ddl-auto=validate, Hibernate checks that an orders table exists with customer_email, total and placed_at columns of compatible types. The explicit @Column names keep the entity and the migration easy to compare side by side.

Example · java
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.Instant;
import org.springframework.data.jpa.repository.JpaRepository;

@Entity
@Table(name = "orders")
public class CustomerOrder {

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

    @Column(name = "customer_email", nullable = false, length = 320)
    private String customerEmail;

    @Column(nullable = false, precision = 12, scale = 2)
    private BigDecimal total;

    @Column(name = "placed_at", nullable = false)
    private Instant placedAt;

    protected CustomerOrder() {
    }

    public CustomerOrder(String customerEmail, BigDecimal total, Instant placedAt) {
        this.customerEmail = customerEmail;
        this.total = total;
        this.placedAt = placedAt;
    }

    public Long getId() {
        return id;
    }
}

interface CustomerOrderRepository extends JpaRepository<CustomerOrder, Long> {
}

The migration that owns the table

Flyway runs this once and records it. Later changes go in V2, V3 and so on, so every environment ends up with the same schema. Hibernate's validate step then confirms the entity above still agrees with it.

Example · sql
-- src/main/resources/db/migration/V1__create_orders.sql
create table orders (
    id             bigint generated always as identity primary key,
    customer_email varchar(320)   not null,
    total          numeric(12, 2) not null,
    placed_at      timestamp with time zone not null
);

create index orders_customer_email_idx on orders (customer_email);

A test that catches schema drift

The slice starts Flyway against a real PostgreSQL, then Hibernate validates every entity. A forgotten migration fails this test in CI instead of failing a deployment. The default Replace.NON_TEST mode of @AutoConfigureTestDatabase keeps the @ServiceConnection DataSource, so the slice does not swap in an embedded database with create-drop that would hide the problem.

Example · java
import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.postgresql.PostgreSQLContainer;

@DataJpaTest(properties = "spring.jpa.hibernate.ddl-auto=validate")
@Testcontainers
class SchemaMatchesEntitiesTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:18-alpine");

    @Autowired
    JdbcClient jdbc;

    @Test
    void migrationsProduceTheSchemaTheEntitiesExpect() {
        // Reaching this line means Flyway ran and Hibernate validation passed.
        Integer migrations = jdbc.sql("select count(*) from flyway_schema_history")
                .query(Integer.class)
                .single();
        assertThat(migrations).isPositive();
    }
}

Discussion

  • Be the first to comment on this lesson.