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.
spring.jpa.hibernate.ddl-auto=validate- Added in Spring Boot Spring Boot 4.1 (verified present)
Parameters
| Parameter | Type | Description |
|---|---|---|
$noneoptional | value= none (non-embedded database) | Do nothing. The default for a non-embedded database, and the default whenever Flyway or Liquibase manages the schema. |
$validateoptional | value | Compare entities with the existing tables and fail startup on a mismatch. Makes no changes. |
$updateoptional | value | Add missing tables and columns. Never drops or alters existing ones. |
$createoptional | value | Drop and recreate the schema at startup, destroying existing data. |
$create-dropoptional | value= 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-onlyoptional | value | Create the schema without dropping anything first. |
$dropoptional | value | Drop the schema at startup. |
$truncateoptional | value | Delete 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
| Value | Use it for |
|---|---|
create-drop | Throwaway databases in tests and demos |
update | A personal scratch database while modelling, never shared ones |
validate | Any environment where Flyway owns the schema: fail fast on drift |
none | Production, 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
# 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.
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.
-- 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.
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