ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

告别手写SQL建表:基于JPA实体驱动的自动建表实践与避坑指南

告别手写SQL建表:基于JPA实体驱动的自动建表实践与避坑指南

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,不妨从下一个新项目开始试试实体驱动建表,感受一下“改完实体重启就完事”的流畅感。但记住那条底线:生产环境的表结构,永远要有人把关。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进