Schema Generation with ddl-auto

What ddl-auto Does

spring.jpa.hibernate.ddl-auto tells Hibernate what to do with tables at startup:

ValueAt startupAt shutdownUse it for
noneNothingNothingProduction (with Flyway/Liquibase)
validateChecks tables match entities; fails if notNothingProduction safety check
updateAdds missing tables/columns (never drops)NothingLocal development
createDrops and recreates all tablesNothingDemos, experiments
create-dropDrops and recreatesDrops tablesTests; default for embedded H2

Note: Never use create, create-drop or even update on a production database. create deletes all data on every restart, and update cannot rename or remove columns safely. Use schema migrations (Flyway) — covered in the advance level.

Generated DDL

Startup log

With ddl-auto: create and SQL logging, Hibernate prints the DDL it runs for the Product entity (MySQL):

spring.jpa.hibernate.ddl-auto=create

Example output

Hibernate:
    drop table if exists products
Hibernate:
    create table products (
        price decimal(10,2) not null,
        stock integer not null,
        id bigint not null auto_increment,
        category varchar(50),
        description varchar(2000),
        name varchar(150) not null,
        primary key (id)
    ) engine=InnoDB

Schema Generation in Miniature

Example

Hibernate builds DDL from your class metadata and the dialect type mapping. This toy generator does the same for a record using reflection:

import java.lang.reflect.RecordComponent;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;

public class Main {
    static final Map<Class<?>, String> TYPES = Map.of(
        Long.class, "bigint", String.class, "varchar(255)", Integer.class, "integer",
        BigDecimal.class, "decimal(38,2)", LocalDateTime.class, "datetime(6)", Boolean.class, "bit");

    public static void main(String[] args) {
        System.out.println(createTable(Product.class));
        System.out.println(createTable(Category.class));
    }

    static String createTable(Class<? extends Record> type) {
        List<String> cols = new ArrayList<>();
        for (RecordComponent rc : type.getRecordComponents()) {
            String col = toSnakeCase(rc.getName()) + " " + TYPES.get(rc.getType());
            if (rc.getName().equals("id")) col += " not null auto_increment";
            cols.add("    " + col);
        }
        cols.add("    primary key (id)");
        return "create table " + toSnakeCase(type.getSimpleName()) + " (" + System.lineSeparator()
            + String.join("," + System.lineSeparator(), cols) + System.lineSeparator() + ")";
    }

    // Spring Boot's naming strategy: camelCase -> snake_case
    static String toSnakeCase(String s) {
        StringBuilder sb = new StringBuilder();
        for (char c : s.toCharArray()) {
            if (Character.isUpperCase(c) && sb.length() > 0) sb.append('_');
            sb.append(Character.toLowerCase(c));
        }
        return sb.toString();
    }
}

record Product(Long id, String name, BigDecimal price, Integer stock, LocalDateTime createdAt) {}
record Category(Long id, String name, String slug, Boolean active) {}

Output

create table product (
    id bigint not null auto_increment,
    name varchar(255),
    price decimal(38,2),
    stock integer,
    created_at datetime(6),
    primary key (id)
)
create table category (
    id bigint not null auto_increment,
    name varchar(255),
    slug varchar(255),
    active bit,
    primary key (id)
)

Notice createdAt became created_at. Spring Boot configures Hibernate with CamelCaseToUnderscoresNamingStrategy, so you rarely need @Column(name = ...) just for snake_case.