1. 从“手写建表”到“实体驱动”的思维转变
写了十年SQL,我经历过那个把DDL语句当宝贝一样存在txt文档里的年代。每次新项目启动,第一件事就是打开数据库客户端,对着需求文档一个字一个字地敲CREATE TABLE,字段类型、长度、索引、注释,一个都不能少。那时候觉得这是基本功,是“数据库工程师的尊严”。直到有一次,一个紧急需求要在两天内上线一套新的业务模块,我照例开始建表,结果光是把二十多张表的DDL写完、跑通、对齐字段,就花掉了大半天。更崩溃的是,下午产品经理过来说“那个订单表的收货地址要拆成省市区三个字段”,我当场就想把键盘砸了——这意味着我要改DDL、改实体类、改Mapper XML、改前端VO,任何一个环节漏了就是运行时异常。
那次之后我开始认真思考一个问题:建表这件事,到底应该是数据库优先,还是代码优先?
传统做法是数据库优先:先设计ER图,再写DDL建表,然后根据表结构反向生成实体类。这个流程在大型项目、多人协作、DBA严格管控的场景下确实有它的道理——数据库是核心资产,表结构必须稳定、可审计。但对于快速迭代的业务系统,尤其是中小型项目、敏捷开发团队,这套流程的摩擦成本太高了。每次字段变更都要走一遍“改表→改实体→改映射”的链路,任何一个环节的延迟都会拖慢整个迭代节奏。
而“从建表到实体一行没敲”这个项目,核心思路就是把这个链路反过来:以Java实体类为唯一事实来源,让框架自动推导出数据库表结构。你只需要在实体类上标注好字段和约束,剩下的建表语句生成、表结构同步、甚至数据初始化,全部交给框架去处理。这不是偷懒,而是一种工程效率上的取舍——把重复性的、机械性的DDL编写工作交给工具,把精力留给真正的业务逻辑。
这个思路能解决什么问题?最直接的就是字段变更的响应速度。产品说加个字段,你只需要在实体类里加一个属性,重启应用,表结构自动同步。不需要打开数据库客户端,不需要写ALTER TABLE,不需要担心开发环境和生产环境的表结构不一致。对于用Spring Data JPA或MyBatis-Plus的团队来说,这个能力几乎是刚需。
适合谁来参考?如果你正在用Spring Boot + JPA/MyBatis-Plus做业务系统,团队规模在2到10人之间,迭代节奏快,数据库变更频繁,那这套方案能帮你省下大量时间。但如果你在金融、电信这类对数据库变更管控极严的行业,或者团队有专职DBA统一管理表结构,那还是老老实实走数据库优先的流程,别想着让应用自动改表——生产环境的表结构变更必须经过审核和灰度。
2. 核心机制拆解:框架到底是怎么“猜”出建表语句的
2.1 JPA的ddl-auto:从实体到DDL的自动推导
Spring Data JPA的ddl-auto配置是这个能力的基石。它的原理并不复杂:Hibernate在启动时会扫描所有标注了@Entity的类,解析每个字段的类型、长度、是否为空、是否唯一等信息,然后根据当前使用的数据库方言(Dialect)生成对应的DDL语句。
ddl-auto有几个可选值,每个值的含义和适用场景完全不同:
| 配置值 | 行为 | 适用场景 | 风险 |
|---|---|---|---|
none | 不做任何操作 | 生产环境 | 无 |
validate | 只校验实体与表是否一致,不一致报错 | 生产环境 | 启动时可能因字段不匹配而失败 |
update | 自动新增字段、索引,不删除已有字段 | 开发/测试环境 | 不会删除字段,可能积累冗余列 |
create | 每次启动删除旧表重新创建 | 单元测试 | 数据全部丢失 |
create-drop | 启动创建,关闭删除 | 单元测试 | 数据全部丢失 |
我一般建议在开发环境用update,生产环境用validate。update的好处是它只做“增量”操作——你加字段它加列,你加索引它建索引,但不会删列。这听起来很安全,但有个坑:如果你把实体类里的某个字段删了,数据库里的列还在,只是应用不再映射它。时间长了,表里会积累一堆“僵尸列”。所以定期做一次表结构比对和清理是必要的。
validate则是生产环境的守门员。它不会改表,但会在启动时检查实体和表是否匹配。如果有人在生产库手动加了字段但忘了改实体,或者实体改了但忘了发DDL,启动就会直接失败。这个“失败”其实是好事——总比运行时才发现字段映射错误要好。
2.2 MyBatis-Plus的逆向能力:从实体生成SQL
MyBatis-Plus本身不提供自动建表功能,但它有一套强大的代码生成器,可以反向操作:根据数据库表生成实体类。不过社区里有人做了扩展,通过解析实体类的注解(@TableName、@TableField、@TableId)来生成建表SQL。这个思路和JPA类似,只是实现方式不同。
MyBatis-Plus的实体注解体系比JPA更轻量,核心就几个:
@TableName:指定表名@TableId:标注主键,可指定生成策略(自增、UUID、雪花算法等)@TableField:标注普通字段,可指定列名、是否忽略、填充策略等
如果要实现“实体驱动建表”,需要自己写一个解析器,扫描这些注解,然后拼接DDL。这个工作量不大,但需要处理不同数据库的方言差异——MySQL的AUTO_INCREMENT、PostgreSQL的SERIAL、Oracle的SEQUENCE,写法都不一样。
2.3 为什么选择实体驱动而不是数据库驱动
这个问题的本质是:谁应该是表结构的“真相源”?
数据库驱动的逻辑是:DBA或架构师设计表结构,开发人员根据表写实体。好处是表结构稳定、可控、可审计。坏处是变更链路长,开发人员没有主动权。
实体驱动的逻辑是:开发人员根据业务需求设计实体,框架根据实体生成表。好处是变更快、一致性强、开发效率高。坏处是表结构可能不够优化,索引设计可能不到位,需要开发人员有足够的数据库素养。
我个人的经验是:在业务快速迭代阶段,实体驱动更合适;在系统稳定期,可以逐步转向数据库驱动,由DBA介入做索引优化和表结构治理。两者不是对立的,而是不同阶段的策略选择。
3. 实操落地:从零搭建一套实体驱动建表的工程
3.1 环境准备与依赖配置
先明确技术栈:Spring Boot 3.x + Spring Data JPA + MySQL 8.x。如果你用的是Spring Boot 2.x,配置基本一致,只是部分包名有变化。
pom.xml里需要引入的核心依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>如果你用的是MyBatis-Plus,把spring-boot-starter-data-jpa换成mybatis-plus-boot-starter即可,但自动建表需要额外引入mybatis-plus-generator或自己实现解析逻辑。
application.yml的关键配置:
spring: datasource: url: jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect这里有几个细节值得展开。ddl-auto: update是核心,它让Hibernate在启动时自动比对实体和表结构,生成并执行DDL。show-sql: true和format_sql: true是开发阶段的调试利器,你可以在控制台看到Hibernate实际执行的建表语句,方便排查问题。
注意:
ddl-auto的值不要在生产环境用update。生产环境的表结构变更必须走审核流程,用validate做启动校验就够了。
3.2 实体类设计:字段类型与约束的映射规则
实体类的设计直接决定了生成的表结构。每个注解、每个字段类型的选择,都会影响最终的DDL。下面是一个典型的订单实体:
@Entity @Table(name = "t_order", indexes = { @Index(name = "idx_user_id", columnList = "user_id"), @Index(name = "idx_create_time", columnList = "create_time") }) public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "order_no", nullable = false, unique = true, length = 32) private String orderNo; @Column(name = "user_id", nullable = false) private Long userId; @Column(name = "amount", precision = 12, scale = 2, nullable = false) private BigDecimal amount; @Column(name = "status", nullable = false, length = 16) @Enumerated(EnumType.STRING) private OrderStatus status; @Column(name = "remark", length = 255) private String remark; @Column(name = "create_time", nullable = false, updatable = false) private LocalDateTime createTime; @Column(name = "update_time") private LocalDateTime updateTime; @PrePersist public void prePersist() { this.createTime = LocalDateTime.now(); this.updateTime = LocalDateTime.now(); } @PreUpdate public void preUpdate() { this.updateTime = LocalDateTime.now(); } }这段代码生成的DDL大致是这样的:
CREATE TABLE t_order ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, amount DECIMAL(12,2) NOT NULL, status VARCHAR(16) NOT NULL, remark VARCHAR(255), create_time DATETIME(6) NOT NULL, update_time DATETIME(6), PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_create_time (create_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有几个映射规则需要记住:
Long映射为BIGINT,Integer映射为INT,String映射为VARCHAR,BigDecimal映射为DECIMAL@Column(length = 32)控制VARCHAR的长度,不指定时默认255@Column(precision = 12, scale = 2)控制DECIMAL的精度和小数位数@Enumerated(EnumType.STRING)让枚举以字符串形式存储,而不是默认的ORDINAL(数字序号)。强烈建议用STRING,因为ORDINAL在枚举顺序变化时会导致数据错乱@Index注解可以声明索引,Hibernate会在建表时一并创建@PrePersist和@PreUpdate是JPA的生命周期回调,用来填充创建时间和更新时间,比数据库的DEFAULT CURRENT_TIMESTAMP更灵活
实操心得:
LocalDateTime在MySQL中默认映射为DATETIME(6),精度到微秒。如果你不需要这么高的精度,可以在@Column里指定columnDefinition = "DATETIME"来覆盖默认行为。
3.3 启动即建表:一次完整的运行记录
配置好之后,启动Spring Boot应用,控制台会输出Hibernate执行的DDL。我第一次跑的时候特意观察了日志,大致是这样的:
Hibernate: create table t_order ( id bigint not null auto_increment, amount decimal(12,2) not null, create_time datetime(6) not null, order_no varchar(32) not null, remark varchar(255), status varchar(16) not null, update_time datetime(6), user_id bigint not null, primary key (id) ) engine=InnoDB Hibernate: alter table t_order add constraint uk_order_no unique (order_no) Hibernate: create index idx_user_id on t_order (user_id) Hibernate: create index idx_create_time on t_order (create_time)注意Hibernate的执行顺序:先建表,再加唯一约束,最后建索引。这个顺序是有讲究的——唯一约束和索引在表创建后单独添加,避免建表语句过于复杂导致某些数据库解析失败。
如果你在开发过程中修改了实体类,比如给Order加了一个payTime字段,重启应用后Hibernate会执行:
alter table t_order add column pay_time datetime(6)这就是update模式的核心价值:增量同步,不破坏已有数据。你不需要手动写ALTER TABLE,也不需要担心忘记同步表结构。
但这里有个坑:Hibernate的update模式不会删除列,也不会修改列的类型。如果你把remark字段的长度从255改成500,Hibernate不会自动执行ALTER TABLE ... MODIFY COLUMN。它只会新增列,不会修改或删除列。所以字段类型的变更还是需要手动处理,或者用Flyway/Liquibase这类数据库迁移工具来管理。
3.4 生产环境的正确姿势:validate + 迁移工具
开发环境用update很爽,但生产环境绝对不能这么干。生产环境的表结构变更必须可控、可审计、可回滚。我的做法是:
- 开发环境:
ddl-auto: update,享受自动同步的便利 - 测试环境:
ddl-auto: validate,启动时校验实体与表是否一致 - 生产环境:
ddl-auto: validate+ Flyway管理DDL脚本
Flyway的工作方式是:你在resources/db/migration目录下放SQL脚本,命名规则是V1__init.sql、V2__add_pay_time.sql,应用启动时Flyway会按顺序执行这些脚本,并在数据库里记录执行历史。这样每次变更都有据可查,回滚也有依据。
pom.xml引入Flyway:
<dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> </dependency> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-mysql</artifactId> </dependency>application.yml配置:
spring: flyway: enabled: true locations: classpath:db/migration baseline-on-migrate: true这样开发阶段用JPA自动建表快速迭代,稳定后把DDL导出成Flyway脚本,生产环境用Flyway执行。两套机制配合,既保证了开发效率,又保证了生产安全。
4. 踩坑实录:那些文档里不会写的教训
4.1 字段类型映射的“惊喜”
JPA的字段类型映射有一套默认规则,但这些规则在不同数据库、不同Hibernate版本下可能有差异。我踩过最坑的一次是String字段映射为VARCHAR(255),但MySQL的utf8mb4字符集下,VARCHAR(255)的索引长度是255*4=1020字节,超过了InnoDB单列索引767字节的限制(在COMPACT行格式下)。结果就是建索引时直接报错。
解决办法有两种:一是把length改小,比如VARCHAR(191)(191*4=764,刚好在限制内);二是把行格式改成DYNAMIC,这样索引长度限制会放宽到3072字节。我一般选后者,在application.yml里加:
spring: jpa: properties: hibernate: dialect: org.hibernate.dialect.MySQL8Dialect然后在数据库连接URL里加上&sessionVariables=innodb_default_row_format=DYNAMIC,或者直接在建表时指定ROW_FORMAT=DYNAMIC。
另一个坑是Boolean类型。在MySQL中,Hibernate默认把Boolean映射为BIT(1),但有些团队习惯用TINYINT(1)。如果你在实体里用了Boolean,但数据库里是TINYINT,validate模式启动时会报类型不匹配。解决办法是用@Column(columnDefinition = "TINYINT(1)")显式指定。
4.2 枚举映射的ORDINAL陷阱
前面提到过,@Enumerated默认是ORDINAL,也就是按枚举的声明顺序存数字。这个默认值极其危险。假设你有一个订单状态枚举:
public enum OrderStatus { CREATED, PAID, SHIPPED, COMPLETED, CANCELLED }存到数据库里是0、1、2、3、4。后来业务调整,在PAID后面插入了一个REFUNDING:
public enum OrderStatus { CREATED, PAID, REFUNDING, SHIPPED, COMPLETED, CANCELLED }这时候数据库里原来的SHIPPED(值2)会被解读为REFUNDING,COMPLETED(值3)会被解读为SHIPPED。整个状态机全乱了。
所以枚举字段一定要用@Enumerated(EnumType.STRING)。存字符串虽然占空间,但可读性强,顺序变化不影响数据。如果担心存储空间,可以用@Convert自定义转换器,把枚举映射为短字符串或数字编码,但那是另一个话题了。
4.3 索引与唯一约束的命名冲突
Hibernate自动生成的索引名和唯一约束名有时候会冲突。比如你在两个实体里都定义了@Index(name = "idx_create_time"),Hibernate会尝试创建两个同名的索引,第二个就会失败。
解决办法是给索引名加上表名前缀,比如idx_order_create_time、idx_user_create_time。或者干脆不指定name,让Hibernate自动生成,但自动生成的名字可读性差,排查问题时不方便。
唯一约束也是类似。@Column(unique = true)会让Hibernate生成一个唯一约束,但约束名是自动生成的,不同数据库的命名规则不一样。如果你需要精确控制约束名,用@Table(uniqueConstraints = @UniqueConstraint(name = "uk_order_no", columnNames = "order_no"))。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查思路 | 解决方案 |
|---|---|---|---|
| 启动时报“Table doesn‘t exist” | ddl-auto配置为none或validate,但表未创建 | 检查ddl-auto配置和数据库连接 | 开发环境改为update,或手动执行DDL |
| 启动时报“Schema-validation: missing column” | 实体字段与表结构不一致 | 对比实体和表的字段列表 | 用update同步,或手动ALTER TABLE |
| 建索引时报“Specified key was too long” | VARCHAR长度超过索引限制 | 检查字段长度和字符集 | 缩短长度或改用DYNAMIC行格式 |
| 枚举字段数据错乱 | 使用了ORDINAL映射 | 检查@Enumerated注解 | 改为EnumType.STRING |
update模式不修改列类型 | Hibernate的update只做增量 | 确认是否修改了字段类型 | 手动执行ALTER TABLE或用Flyway |
| 多数据源下建表失败 | 默认只对主数据源生效 | 检查多数据源配置 | 为每个数据源单独配置JPA属性 |
避坑技巧:在开发阶段,我习惯把
show-sql打开,每次启动时扫一眼Hibernate执行的DDL。如果看到意料之外的ALTER TABLE或CREATE INDEX,说明实体定义有问题,及时修正。这个习惯帮我避免了很多“上线才发现表结构不对”的事故。
5. 进阶玩法:让实体驱动走得更远
5.1 自定义命名策略:从驼峰到下划线
JPA默认的命名策略是把驼峰字段名直接作为列名,比如orderNo对应orderNo。但数据库规范通常要求下划线命名,orderNo应该对应order_no。虽然可以用@Column(name = "order_no")逐个指定,但字段多了很繁琐。
更好的做法是配置全局命名策略:
spring: jpa: hibernate: naming: physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy这样orderNo会自动映射为order_no,createTime映射为create_time,不需要每个字段都写@Column。只有需要特殊处理时才用@Column覆盖。
5.2 审计字段自动填充
创建时间、更新时间、创建人、更新人这几个字段几乎每个表都有。与其在每个实体里重复写@PrePersist,不如用JPA的审计功能统一处理。
首先在启动类上开启审计:
@EnableJpaAuditing @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }然后定义一个基础实体:
@MappedSuperclass @EntityListeners(AuditingEntityListener.class) public abstract class BaseEntity { @CreatedDate @Column(name = "create_time", nullable = false, updatable = false) private LocalDateTime createTime; @LastModifiedDate @Column(name = "update_time") private LocalDateTime updateTime; @CreatedBy @Column(name = "create_by", length = 64) private String createBy; @LastModifiedBy @Column(name = "update_by", length = 64) private String updateBy; }所有业务实体继承BaseEntity,审计字段自动填充。@CreatedBy和@LastModifiedBy需要实现AuditorAware接口来提供当前用户信息,可以从SecurityContext或ThreadLocal里取。
5.3 多数据源下的建表策略
多数据源场景下,JPA的自动建表默认只对主数据源生效。如果你有两个数据源,需要为每个数据源单独配置EntityManagerFactory和TransactionManager,并分别指定ddl-auto策略。
配置类大致是这样的:
@Configuration @EnableJpaRepositories( basePackages = "com.example.repo.primary", entityManagerFactoryRef = "primaryEntityManagerFactory", transactionManagerRef = "primaryTransactionManager" ) public class PrimaryDataSourceConfig { @Primary @Bean(name = "primaryEntityManagerFactory") public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory( EntityManagerFactoryBuilder builder, @Qualifier("primaryDataSource") DataSource dataSource) { Map<String, Object> properties = new HashMap<>(); properties.put("hibernate.hbm2ddl.auto", "update"); properties.put("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect"); return builder .dataSource(dataSource) .packages("com.example.entity.primary") .persistenceUnit("primary") .properties(properties) .build(); } }第二个数据源类似,只是把@Primary去掉,包名和Bean名换成对应的。这样两个数据源各自管理自己的表结构,互不干扰。
5.4 从实体生成DDL脚本导出
有时候你需要把实体对应的DDL导出成SQL文件,比如给DBA审核,或者部署到没有自动建表权限的环境。Hibernate提供了SchemaExport工具类,可以在代码里调用:
public class DdlExporter { public static void main(String[] args) { Map<String, Object> settings = new HashMap<>(); settings.put("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect"); settings.put("hibernate.hbm2ddl.auto", "none"); StandardServiceRegistry registry = new StandardServiceRegistryBuilder() .applySettings(settings) .build(); MetadataSources sources = new MetadataSources(registry); sources.addAnnotatedClass(Order.class); sources.addAnnotatedClass(User.class); Metadata metadata = sources.buildMetadata(); SchemaExport export = new SchemaExport(); export.setOutputFile("ddl/export.sql"); export.setFormat(true); export.setDelimiter(";"); export.createOnly(EnumSet.of(TargetType.SCRIPT), metadata); } }这段代码会扫描指定的实体类,生成DDL并输出到ddl/export.sql。你可以把这个文件交给DBA审核,或者在CI/CD流程里自动生成并归档。
实操心得:导出DDL时记得把
hibernate.hbm2ddl.auto设为none,否则SchemaExport可能会尝试连接数据库执行语句。createOnly方法的第一个参数指定只生成脚本不执行,第二个参数是元数据。
6. 这套方案适合你吗:场景匹配与边界说明
6.1 最适合的场景
这套“实体驱动建表”的方案,在以下几种场景下收益最明显:
快速迭代的互联网业务系统。需求一周一变,字段三天一加,用实体驱动可以做到“改代码即改表”,省去大量沟通和等待成本。我经历过一个项目,从零到上线两个月,表结构变更超过五十次,如果没有自动建表,光DDL编写和同步就要多花至少三天。
中小团队、全栈开发。没有专职DBA,后端开发自己管数据库。这种情况下,让框架处理建表逻辑,开发人员只需要关注实体设计,减少上下文切换。
原型验证和Demo开发。快速搭一个可运行的系统,验证业务逻辑,不需要在表结构上花太多时间。等业务跑通了,再把DDL导出成正式脚本,走正规流程。
单元测试和集成测试。用create-drop模式,每次测试启动时创建全新的表结构,测试结束后销毁,保证测试环境干净、可重复。
6.2 不适合的场景
金融、电信等强管控行业。表结构变更必须经过DBA审核、影响评估、灰度发布,不能让应用自动改表。这类场景下,ddl-auto只能设为validate或none,DDL变更走专门的工单流程。
已有大量历史数据的系统。自动建表只适合新表或开发阶段的表。对于已有几千万行数据的表,任何ALTER TABLE都可能锁表,必须用pt-online-schema-change或gh-ost这类工具做在线变更。
多团队共享数据库。如果多个应用共享同一个数据库,每个应用都自动建表会导致表结构冲突。这种情况下,表结构应该由独立的DDL管理模块统一维护。
对索引优化要求极高的场景。自动生成的索引往往只覆盖基本查询,复杂的复合索引、覆盖索引、部分索引需要手动设计和调优。自动建表可以作为起点,但最终的索引方案必须人工介入。
6.3 从自动建表到数据库治理的演进路径
我的建议是分三个阶段:
第一阶段:开发环境全自动。ddl-auto: update,享受快速迭代的便利。这个阶段的目标是跑通业务,表结构可以粗糙,索引可以简单。
第二阶段:测试环境校验。ddl-auto: validate,配合Flyway管理DDL脚本。每次变更都生成迁移脚本,保证测试环境和生产环境的一致性。
第三阶段:生产环境治理。由DBA或架构师定期审查表结构,优化索引,清理冗余列,制定分库分表策略。自动建表退居二线,只作为开发辅助工具。
这个演进路径的核心逻辑是:开发效率优先,逐步过渡到稳定性优先。没有一套方案是万能的,关键是知道在什么阶段用什么工具。
7. 我个人的几条实操建议
第一,永远不要在生成环境用ddl-auto: update。这条规则没有例外。我见过太多因为自动改表导致生产事故的案例,轻则锁表几分钟,重则数据丢失。生产环境的表结构变更必须走审核流程。
第二,实体类就是文档。当你用实体驱动建表时,实体类成了表结构的唯一描述。所以实体类的注释、字段命名、注解配置必须清晰规范。我习惯在实体类头部写一段注释,说明这个表的业务含义、关键字段、索引设计思路。这样后来的人看代码就能理解表结构,不需要去翻数据库。
第三,定期导出DDL做比对。即使开发环境用自动建表,也建议每周导出一次DDL,和上周的版本做diff。这样能及时发现意外的表结构变更,比如某个字段被误删、某个索引被覆盖。
第四,索引设计不能完全交给框架。Hibernate只会根据@Index注解建索引,它不会分析你的查询模式。所以慢查询优化、复合索引设计、覆盖索引选择,这些还是需要人工介入。我的做法是:开发阶段用自动建表快速跑通,上线前用EXPLAIN分析核心查询,手动补充必要的索引。
第五,枚举字段用STRING,时间字段用LocalDateTime,金额字段用BigDecimal。这三条是我踩了无数坑之后总结出来的铁律。ORDINAL枚举会因顺序变化导致数据错乱,Date和Timestamp有时区问题,Double做金额计算会有精度丢失。用对类型,能避免80%的数据问题。
这套方案我用了三年多,从最初的“试试看”到现在的“标配”,中间踩过的坑、填过的雷,基本都写在这了。如果你也在用JPA或MyBatis-Plus,不妨从下一个新项目开始试试实体驱动建表,感受一下“改完实体重启就完事”的流畅感。但记住那条底线:生产环境的表结构,永远要有人把关。