启动服务时按实体自动建表,注解零改动复用

实体驱动建表听起来不新鲜——Hibernate 的 ddl-auto、不少脚手架都能干。真正卡住的人,多半不是"会不会生成 CREATE TABLE",而是:
- 项目根本不在 Hibernate / JPA Provider 体系里,却已经有一套带
@Entity的类; - 只要 DDL 文本 给 DBA,不想应用一启动就改库;
- 同一套实体要出 MySQL、PostgreSQL、达梦、Oracle,乃至 Hive / ClickHouse 好几份,注释和自增写法全不一样。
jkit-sql 的切入点是:从实体扫出模型,按方言生成 DDL(以及增删改查);需要连库执行时,再交给 jkit-sql-auto。它认 jkit 自己的注解,也认 MyBatis-Plus、以及 javax.persistence / jakarta.persistence 里的标准映射注解——没有对这些 API 的编译依赖。
一、先澄清:@Entity 不是"Hibernate 专属"
文里常说"JPA 注解",容易让人以为必须上 Hibernate / EclipseLink 全套。更准确的说法是:
@Entity / @Table / @Column / @Id / @GeneratedValue 等定义在 Jakarta Persistence API(历史包名 javax.persistence,现为 jakarta.persistence)里,也就是大家说的 persistence-api。Hibernate、EclipseLink 是它的实现(Provider);Spring Data JPA、各种脚手架只是在用这套 API。
所以你会看到三种很常见、却容易被混为一谈的情况:
- 有 persistence-api 注解,但没有 JPA Provider——类上挂着
@Entity,持久化却走 MyBatis / JdbcTemplate; - 只有 MyBatis-Plus 注解——
@TableName/@TableId,和 persistence-api 无关; - 两边都有——存量实体两套注解混用,迁移期最麻烦。
jkit 的做法是:@Entity / @Table / @Column / @Id 等按全限定名认 javax.persistence.* 与 jakarta.persistence.*;MyBatis-Plus 同理认其包名下的注解。只有表/列注释那类 Comment 注解,才按简单类名匹配(不绑包名)。全程无编译依赖——不引入 Hibernate / Spring Data / MyBatis-Plus。你不用为了"能 ddl-auto"硬塞一个 Provider,也不用为了生成 DDL 把实体改写成 jkit 注解。
二、为什么还要单独搞"生成建表 SQL / 自动建表"
下面这些痛点,比"再写一遍 CREATE TABLE"更常见:
1. 只要脚本,不要启动改库
测试库可以自动建;生产往往要求:生成 DDL → DBA 评审 → 进 Flyway / Liquibase。很多工具绑死了"启动时连库执行",缺一截"只出文本"的能力。
2. 同一套实体,多套库
信创迁移、双写验证、本地 MySQL + 预发达梦,都要同一模型、不同方言的注释 / 自增 / 序列。手写两份 DDL 很快漂移。
3. 开发环境想快,生产环境要稳
本地希望改实体就出表;生产只允许"追加列",禁止默默改类型、删列。缺"只追加不收缩 + dryRun"时,自动建表不敢开。
4. 集成测试要临时库表
Testcontainers / 嵌入式库每次拉起都要 schema。从实体生成 DDL,比维护一份永远过期的 schema.sql 省心。
5. 微服务 / 多模块实体分散
实体散落在多个 jar,建表顺序还要顾外键。需要按包扫描,并按外键依赖排序后再出脚本。
6. 注释和命名要给 DBA 看
中文表注释、列注释、索引名长度(经典 Oracle 30 字符)——生成器如果不管,评审轮次会明显变多。
7. 已经有注解,不想为了建表换栈
类上已是 persistence-api 或 MP 注解。为了 ddl-auto 引入 Hibernate,或把注解全部改成另一套,成本都高于"读现有注解生成 SQL"。
这些场景里,你要的通常是两截能力拆开:SqlEntities 负责生成;sql-auto 负责(可选地)执行。

三、注解兼容性:一张表说清
| 来源 | 典型注解 | 说明 |
|---|---|---|
| jkit 原生 | @SqlTable / @SqlId / @SqlColumn / @SqlGenerated | 无第三方语义依赖 |
persistence-api(javax / jakarta) | @Entity / @Table / @Column / @Id / @GeneratedValue 等 | 按全限定名识别,无编译依赖 |
| MyBatis-Plus | @TableName / @TableId / @TableField | 同上;exist=false 会跳过 |
另外还认:任意简单名为 Comment 的注释注解、Hibernate @ColumnDefault(有则读,无则忽略),以及 persistence-api 的 @Index(含 @Table(indexes=…))、@Enumerated / @Embedded 等(详见文档)。List / Set / @OneToMany 默认不建列。

原生注解示例:
import com.alianga.jkit.sql.entity.SqlEntities;
import com.alianga.jkit.sql.entity.SqlTable;
import com.alianga.jkit.sql.entity.SqlId;
import com.alianga.jkit.sql.entity.SqlGenerated;
import com.alianga.jkit.sql.entity.SqlColumn;
import com.alianga.jkit.sql.SqlDialect;
@SqlTable(name = "demo_user")
public class DemoUser {
@SqlId @SqlGenerated Long id;
@SqlColumn(name = "user_name", length = 32, nullable = false) String name;
Integer age;
}
String ddl = SqlEntities.createTable(DemoUser.class, SqlDialect.POSTGRES);
// CREATE TABLE demo_user (id BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY PRIMARY KEY, ...)
存量 persistence-api 实体则不必改注解,直接:
// 类上已是 jakarta.persistence.Entity / Table / Id / Column …
String ddl = SqlEntities.createTable(OrderEntity.class, SqlDialect.MYSQL);
List<Class<?>> all = SqlEntities.scan("com.example.entity");
String batch = SqlEntities.createTables(all, SqlDialect.DAMENG);
四、一套实体,多方言 DDL 形态自适应
注释、自增、序列的写法因库而异,生成器要一次处理:
@SqlTable(name = "t_member", comment = "会员表")
class Member {
@SqlId Long id;
@SqlColumn(comment = "昵称") String nick;
}
SqlEntities.createTable(Member.class, SqlDialect.MYSQL);
// MySQL:内联 COMMENT
SqlEntities.createTable(Member.class, SqlDialect.POSTGRES);
// PG:CREATE TABLE + COMMENT ON TABLE/COLUMN
SqlDialect 目前有 13 个一等方言:MYSQL、POSTGRES、ORACLE、ORACLE12、SQLSERVER、ANSI、H2、DB2、SQLITE、HIVE、CLICKHOUSE、PRESTO、DAMENG。另有一批别名可直接 fromName(...)——例如 tidb / mariadb / gbase / doris / starrocks→MySQL,opengauss / gaussdb / kingbase→Postgres,dm→达梦,trino→Presto——信创和衍生库不必各写一套实体。
自增形态差异大的,先看这几类:
| 方言 | 自增形态(差异明显) |
|---|---|
| MySQL(含 TiDB / MariaDB / GBase 等别名) | AUTO_INCREMENT |
| PostgreSQL(含 openGauss / GaussDB 等别名) | GENERATED … AS IDENTITY |
| Oracle ≤11g | SEQUENCE + TRIGGER(附录输出) |
| Oracle 12c+ | GENERATED … AS IDENTITY |
达梦(dm) | 列上 IDENTITY |
| SQL Server | IDENTITY(1,1) |
其余一等方言(ANSI / H2 / DB2 / SQLITE / HIVE / CLICKHOUSE / PRESTO 等)建表走同一套 SqlDialect,类型、引号、注释按方言适配,不必再维护多份实体。注释写法也会跟着变:MySQL 常内联 COMMENT,PostgreSQL / Oracle 一路多是 COMMENT ON,SQL Server 走扩展属性——生成器一并处理。
边界也要管:字符串 / UUID 主键不应生成 IDENTITY;经典 Oracle 索引名超长要截断。这些是手写多方言 DDL 时最容易漏的点。

五、jkit-sql-auto:可选的"启动时连库"
SqlEntities 只生成 SQL。要对比元数据并执行,用 jkit-sql-auto:
SqlAuto.run(SqlAutoOptions.defaults()
.url("jdbc:mysql://localhost:3306/shop")
.username("root").password("secret")
.packages("com.example.entity")
.mode(SqlAutoMode.UPDATE));
Spring Boot 可加 starter:
jkit:
sql:
auto:
enabled: true
mode: update
packages: com.example.entity
show-sql: true
(jkit-sql-auto-spring-boot-2 / -3 分别对应 Boot 2 / Boot 3。)
常见用法拆开选:
| 目标 | 做法 |
|---|---|
| 只给 DBA 脚本 | SqlEntities.createTable / createTables,不引 auto |
| 本地 / 测试自动对齐 | auto + UPDATE,可开 show-sql |
| 先看再执行 | dryRun(true),打印 plan.sql() |
| 生产受控变更 | 生成文本 → 写入 Flyway;生产勿与 auto UPDATE 双开 |

六、生产默认 UPDATE:只追加不收缩
SqlAutoMode:NONE / VALIDATE / UPDATE(默认) / CREATE / CREATE_DROP。
- UPDATE 默认只追加:新列
ALTER TABLE … ADD;不删列、默认不改类型、不改主键; - 改类型需显式
alter-column: true; - 删多余列需显式
drop-extra-columns: true; dryRun(true)只出计划不执行。
SqlAutoPlan plan = SqlAuto.run(options.dryRun(true));
System.out.println(plan.sql());
自动建表最怕"自作主张弄丢数据"。破坏性操作默认关、显式开,是底线。

七、和 Hibernate ddl-auto、Flyway 怎么分工
- 已有 Hibernate + ddl-auto:开发期可以继续用;jkit 更适合"多方言脚本 / 无 Provider 的工程 / 只生成不执行"。
- MyBatis / 裸 JDBC / 只有 persistence-api 注解:不必为了建表引入 Provider;用 SqlEntities + 可选 sql-auto。
- Flyway / Liquibase:管版本化变更与协作;jkit 管"从实体推导结构"。常见组合是开发/测试用 auto,上线前
dryRun或createTable产出交给迁移脚本。两套同时在生产改表,容易冲突。
原则就一句:代码即 schema 的阶段用实体驱动提效;变更必须受控的阶段用版本化脚本。
建议试用顺序:① createTable 只生成看差异 → ② 测试库 dryRun → ③ 再开 update(只追加)→ ④ 产物进评审,DBA 知情。
定位没变:SQL 文本与 AST、实体与 DDL 之间的轻量工具层——不绑架框架,不替代你的 ORM,把"出 DDL / 对齐表结构"这类脏活收拢。
你那边是更需要"只生成脚本",还是"测试环境自动建、生产走 Flyway"?评论区可以说一下栈(MP / persistence-api / 多库)。
文档:https://jkit.alianga.com/ · 源码:jkit
Q.E.D.


