Skip to content

Auto schema

At startup, scan entity classes, compare them with the live schema, and run CREATE TABLE / ALTER TABLE ADD / CREATE INDEX.

DDL text comes from jkit-sql SqlEntities. This module only inspects DatabaseMetaData and executes. jkit-sql does not run SQL and does not ship a JDBC driver. Runtime stays free of third-party libraries. JDK 8+ (the Boot 3 starter needs JDK 17+).

Pick one path:

ProjectArtifactExtra startup code?
Spring Boot 2.xjkit-sql-auto-spring-boot-2No. Runs once when the app is ready, using the app DataSource
Spring Boot 3.xjkit-sql-auto-spring-boot-3Same
Plain Java / servlet / your own mainjkit-sql-autoYes. Call SqlAuto.run(...) once at startup

All three artifacts pull in jkit and jkit-sql. The host still supplies the JDBC driver.


1. Write an entity

Discovery recognises three styles (by FQCN, no JPA / MyBatis-Plus compile dependency):

  • jkit @SqlTable / @SqlId / @SqlColumn / @SqlGenerated
  • JPA @Entity / @Table / @Id / @Column / @GeneratedValue (javax or jakarta)
  • MyBatis-Plus @TableName / @TableId
java
import com.alianga.jkit.sql.entity.SqlColumn;
import com.alianga.jkit.sql.entity.SqlGenerated;
import com.alianga.jkit.sql.entity.SqlId;
import com.alianga.jkit.sql.entity.SqlTable;

@SqlTable(name = "demo_user", comment = "users", indexes = {"idx_email:email"})
public class User {
    @SqlId
    @SqlGenerated          // integer → AUTO_INCREMENT / IDENTITY; String/UUID stays a plain PK
    private Long id;

    @SqlColumn(name = "user_name", length = 32, nullable = false, comment = "name")
    private String name;

    @SqlColumn(length = 64, unique = true)
    private String email;
}

Existing JPA classes need no extra annotations:

java
@Entity
@Table(name = "file_storage")
public class FileStorage {
    @Id
    @Column(name = "id", length = 32)
    @GeneratedValue(generator = "system-uuid")  // string UUID, not IDENTITY
    private String id;
    // ...
}

Indexes: @SqlTable(indexes = {"col"}) or "name:col1,col2"; JPA @Table(indexes = @Index(...)) is also read. Unnamed indexes become {table}_{col}_idx.

Types, comments and foreign keys: entity scan.


2. Spring Boot

Add the matching starter, set jkit.sql.auto.packages (or entities), and do not call SqlAuto.run from main. It runs once on ApplicationReadyEvent and uses the container DataSource (usually spring.datasource.*).

Boot 2.x (JDK 8+)

xml
<dependency>
    <groupId>com.alianga</groupId>
    <artifactId>jkit-sql-auto-spring-boot-2</artifactId>
    <version>2.0.1</version>
</dependency>

Boot 3.x (JDK 17+)

xml
<dependency>
    <groupId>com.alianga</groupId>
    <artifactId>jkit-sql-auto-spring-boot-3</artifactId>
    <version>2.0.1</version>
</dependency>

application.yml

yaml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/shop
    username: shop
    password: secret

jkit:
  sql:
    auto:
      enabled: true          # default true; false skips the whole step
      mode: update           # none / validate / update / create / create-drop
      packages: com.example.entity
      # entities:
      #   - com.example.entity.User
      show-sql: true

Set at least packages or entities, or the log will say no entities, skip.

Keep mode: update in production (add tables/columns/indexes only). Use create / create-drop in development.

Turn it off with jkit.sql.auto.enabled: false or mode: none.

Boot 2 registers via spring.factories, Boot 3 via AutoConfiguration.imports. Adding the artifact is enough; no @Import.


3. Without Spring

Core artifact only:

xml
<dependency>
    <groupId>com.alianga</groupId>
    <artifactId>jkit-sql-auto</artifactId>
    <version>2.0.1</version>
</dependency>

Supply a JDBC driver yourself. Call once from main, a servlet listener, or any other startup hook:

java
import com.alianga.jkit.sql.auto.SqlAuto;
import com.alianga.jkit.sql.auto.SqlAutoMode;
import com.alianga.jkit.sql.auto.SqlAutoOptions;

public static void main(String[] args) {
    SqlAuto.run(SqlAutoOptions.defaults()
            .url("jdbc:mysql://localhost:3306/shop")
            .username("root")
            .password("secret")
            .packages("com.example.entity")
            .mode(SqlAutoMode.UPDATE));
    // then start the web / business layer
}

With a DataSource:

java
SqlAuto.run(SqlAutoOptions.defaults()
        .dataSource(dataSource)
        .packages("com.example.entity"));

With an open Connection (the caller opens and closes it):

java
SqlAuto.run(connection, SqlAutoOptions.defaults().entities(User.class, Order.class));

Config file, no fluent API

SqlAuto.run() / SqlAutoOptions.fromConfig() read jkit.sql.auto.*. If jkit.sql.auto.url is missing they fall back to spring.datasource.* (a plain Java app can still use a Spring-style yaml).

yaml
jkit:
  sql:
    auto:
      url: jdbc:mysql://localhost:3306/shop
      username: root
      password: secret
      packages: com.example.entity
      mode: update
java
public static void main(String[] args) {
    SqlAuto.run(); // = SqlAuto.run(SqlAutoOptions.fromConfig())
}

Standalone process (jkit-sql-auto, jkit-sql, a JDBC driver and config on the classpath):

text
java com.alianga.jkit.sql.auto.SqlAuto

4. Plan only, do not change the database

SqlAuto.run(options) with dryRun(true) does not open JDBC. The URL is only used to infer the dialect. It plans a full CREATE TABLE as if the schema were empty, so the database does not need to be running:

java
SqlAutoPlan plan = SqlAuto.run(SqlAutoOptions.defaults()
        .dialect(SqlDialect.POSTGRES)   // or .url("jdbc:postgresql://...")
        .packages("com.example.entity")
        .mode(SqlAutoMode.CREATE_DROP)
        .dryRun(true));
List<String> sqls = plan.sql();

To preview ALTER against live tables, pass an open Connection to run(connection, options) (still with dryRun(true) if you only want the plan).

Config key: jkit.sql.auto.dry-run: true.


5. Configuration keys

Prefix is always jkit.sql.auto.. The Spring Boot starters bind the same set; non-Spring fromConfig() reads the same set.

keydefaultmeaning
enabledtruefalse makes run() a no-op
modeupdatenone / validate / update / create / create-drop; also ddl-auto
packagesemptyscan roots (list or comma-separated); aliases package / base-package / base-packages
entitiesemptyentity FQCNs
dialectinferred from URL / DatabaseMetaDatamysql / postgres / oracle / oracle12 / h2 / dm … (SqlDialect.fromName)
urlspring.datasource.urlJDBC URL
usernamespring.datasource.usernameusername
passwordspring.datasource.passwordpassword
driverguessed from URLdriver class; also driver-class-name
fail-fasttrueabort on the first failed DDL
alter-columnfalseALTER/MODIFY on type mismatch
drop-extra-columnsfalsedrop columns not on the entity
create-indextrueemit CREATE INDEX
table-prefixemptyuniform table-name prefix, e.g. t_; applies to create / alter / drop / index / sequence / FK target table
index-prefix-enabledtruewhether auto-derived index names also carry table-prefix (so t_user's index is t_user_idx vs user_idx); an explicit @Index(name=…) is always kept verbatim and ignores this switch
quote-identifiersfalsequote identifiers in the dialect
show-sqltruelog SQL
dry-runfalseplan only
catalog / schemaJDBC defaultDatabaseMetaData lookup scope

The fluent API matches these keys (.mode(SqlAutoMode.UPDATE), .packages("a","b"), .alterColumn(true)). Code-only switches: postgresIdentityStyle(SERIAL), foreignKeys(false), autoIncrement(false).


6. Modes

Mirrors JPA spring.jpa.hibernate.ddl-auto:

SqlAutoModeBehaviour
NONENo-op
VALIDATEThrow SqlAutoException if a table/column is missing or types are incompatible; never mutate
UPDATE (default)Create missing tables, add missing columns/indexes; do not change existing types or drop columns/tables
CREATEDrop managed tables then recreate (dev)
CREATE_DROPSame as CREATE at startup, then drop on JVM shutdown

UPDATE is the production default: expand only. Type mismatches are skipped unless alter-column: true. Extra columns stay unless drop-extra-columns: true.

Drop managed tables explicitly (reverse FK order):

java
SqlAuto.drop(SqlAutoOptions.defaults().url(url).entities(User.class));

7. Dialects

When dialect is unset:

  1. JDBC URL prefix (jdbc:mysql: → MYSQL, jdbc:postgresql: → POSTGRES, jdbc:oracle: → ORACLE, Dameng jdbc:dm: → DAMENG, …)
  2. DatabaseMetaData.getDatabaseProductName() (Oracle 12c+ product names map to ORACLE12)
  3. otherwise MYSQL

jdbc:oracle: infers classic ORACLE (30-character identifiers, SEQUENCE + TRIGGER for autoincrement). For 12c IDENTITY / OFFSET FETCH, set dialect: oracle12.


8. Behaviour and pitfalls

Does

  • Scan packages or an explicit entity list; referenced tables come first by foreign key
  • Missing table → CREATE TABLE (then optional CREATE INDEX, comment extras, Oracle 11g SEQUENCE)
  • Table exists, column missing → ALTER TABLE … ADD
  • Missing index → CREATE INDEX
  • Table/column comments as extra statements (MySQL inline COMMENT, PG/Oracle COMMENT ON, SQL Server sp_addextendedproperty)

Default does not

  • Change existing column types, drop extra columns/tables, rename columns, change primary keys, migrate data
  • Touch procedures / views / triggers (except the Oracle 11g autoincrement trigger)
  • Bundle a JDBC driver

Primary keys / identity

  • Integer + @SqlGenerated / @GeneratedValue(IDENTITY|AUTO)AUTO_INCREMENT / GENERATED … AS IDENTITY / SERIAL
  • String, UUID, @GeneratedValue(generator="system-uuid"), GenerationType.UUIDPRIMARY KEY only (PostgreSQL rejects VARCHAR … IDENTITY)
  • Classic Oracle (ORACLE) integer autoincrement: CREATE SEQUENCE {table}_{column}_seq plus a BEFORE INSERT trigger; CREATE_DROP / drop drop the sequence first

Index names / identifier length

Unnamed indexes are {table}_{col}_idx. Classic Oracle caps identifiers at 30 characters and truncates with a 4-hex hash; ORACLE12 allows 128. Sequence and trigger names use the same rule.

Inherited columns

If a subclass restates a superclass / MappedSuperclass column such as create_time, the name is emitted once (avoids PostgreSQL column specified more than once).

Narrow products

  • Old OpenGauss: .postgresIdentityStyle(SERIAL)
  • GBase 8a: .foreignKeys(false).createIndex(false)
  • DuckDB: .autoIncrement(false).foreignKeys(false).createIndex(false)
  • H2 in-memory CREATE_DROP: put DB_CLOSE_DELAY=-1 on the URL so the database survives the startup connection closing

Released under the Apache License 2.0