资讯动态

Spring Boot导入SQL文件:从自动初始化到Flyway迁移实践

发布时间:2026/10/13 13:32:44 来源:尧图企业网站定制
很多人在Spring Boot项目里都遇到过这个需求上线的时候要执行一份SQL脚本同事发来一个SQL文件要导进开发库或者每次启动测试环境都想自动把表结构和基础数据初始化好。标题里说的springboot导入sql文件完美其实点出了一个真实痛点——把SQL文件弄进去并不难难的是让它稳定、干净、可重复地执行。这篇就把我实际用过的几种方案完整梳理一遍从最简单的启动自动初始化到手动执行脚本的通用方案再到工程化一点的迁移工具全部带上可复现的代码和配置以及我踩过的坑。1. 先理清需求你到底是哪种导入动手之前先想清楚自己的场景方案选错往往是因为需求没分清楚。我见过不少团队只为了启动时自动建几张表就硬上了Flyway也有项目需要执行大SQL脚本却去改application.properties指望系统自动跑结果乱成一团。常见的导入SQL文件需求其实分三类开发/测试环境启动自动初始化希望应用一启动表结构自动创建基础数据自动写入。这种适合用Spring Boot内置的脚本初始化能力。运行时手动执行某个SQL文件运维或开发需要主动触发一份脚本比如修复数据、批量更新。这种适合写一个外部接口或命令行工具调用JDBC去执行文件内容。多环境、多版本迭代下的数据库变更管理开发、测试、生产环境的表结构不一致需要追踪每一次变更。这种属于迁移工具Flyway / Liquibase的活。三种需求对应三种技术路线的组合没必要一上来就搞最重的。注意我见过直接把SQL文件放到resources目录下然后写个监听器在启动时用原生的JDBC逐行读取执行的方案。不是说完全不行而是问题很多脚本里只要出现批量的INSERT语句、存储过程、带分隔符的自定义语句逐行读就容易炸。后面我会讲为什么。2. Spring Boot内置的脚本初始化最省事的自动导入2.1 两行配置搞定开发环境Spring Boot对SQL脚本初始化有原生支持如果你用的是Spring Boot 2.5核心配置是spring.sql.init.*这组属性。基础用法如下spring: sql: init: mode: always schema-locations: classpath:sql/schema.sql >spring: sql: init: mode: alwaysmode有三个值embedded只在嵌入式数据库时执行是默认值always总是执行不管连什么数据库never永不执行手动管理我通常用always配合schema-locations去指定脚本路径。不过如果脚本里是幂等的DDL比如CREATE TABLE IF NOT EXISTS问题不大如果是普通的INSERT INTO每次启动都执行就能看到主键冲突。2.3 制定脚本路径的坑与技巧schema-locations和>spring: sql: init: schema-locations: - classpath:sql/schema.sql - classpath:sql/extend_schema.sql >2025-05-18 10:23:45.123 INFO 5944 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2025-05-18 10:23:45.456 INFO 5944 --- [ main] o.s.jdbc.datasource.init.ScriptUtils : Executing SQL script from class path resource [db/schema.sql] 2025-05-18 10:23:45.789 INFO 5944 --- [ main] o.s.jdbc.datasource.init.ScriptUtils : Executed SQL script from class path resource [db/schema.sql] in 333 ms 2025-05-18 10:23:45.910 INFO 5944 --- [ main] o.s.jdbc.datasource.init.ScriptUtils : Executing SQL script from class path resource [db/data.sql] 2025-05-18 10:23:46.002 INFO 5944 --- [ main] o.s.jdbc.datasource.init.ScriptUtils : Executed SQL script from class path resource [db/data.sql] in 92 ms看到这几行就说明脚本真的被执行了。如果日志里没有ScriptUtils相关的输出先怀疑mode配置是不是不对。3. 手动执行SQL文件通用的工具类方案自动初始化适合开发环境但真正上线或者需要按需执行时我更推荐写一个手动执行SQL文件的工具。这种方案的适用性最广不管脚本是DDL还是DML不管文件多大只要你把文件路径给它放在Spring Boot应用里就能执行。3.1 思路与依赖核心思路是用Spring框架自带的ScriptUtils它的全名是org.springframework.jdbc.datasource.init.ScriptUtils。这个类专门负责解析并执行SQL脚本能处理注释、分隔符、批处理等麻烦事。依赖只有spring-jdbcSpring Boot Web项目里基本都有不用额外引入。3.2 代码实现我写了一个完整的工具类核心方法接收一个DataSource和一个SQL文件路径支持classpath和本地文件系统然后执行。package com.example.common.sql; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.jdbc.datasource.init.ScriptUtils; import javax.sql.DataSource; import java.nio.charset.StandardCharsets; public class SqlFileExecutor { /** * 执行SQL脚本支持classpath和文件系统路径 * * param dataSource 数据源 * param location classpath:sql/xxx.sql 或 file:/opt/sql/xxx.sql */ public static void execute(DataSource dataSource, String location) { Resource resource resolveResource(location); ScriptUtils.executeSqlScript(dataSource.getConnection(), resource); } private static Resource resolveResource(String location) { if (location.startsWith(classpath:)) { String path location.substring(classpath:.length()); return new ClassPathResource(path); } if (location.startsWith(file:)) { String path location.substring(file:.length()); return new FileSystemResource(path); } // 默认按classpath处理 return new ClassPathResource(location); } }调用方式RestController RequestMapping(/admin/sql) public class SqlExecuteController { private final DataSource dataSource; public SqlExecuteController(DataSource dataSource) { this.dataSource dataSource; } PostMapping(/execute) public String execute(RequestParam String location) { try { SqlFileExecutor.execute(dataSource, location); return 执行成功; } catch (Exception e) { return 执行失败 e.getMessage(); } } }这样你只需要把SQL文件放到服务器某个目录通过接口传file:/opt/sql/xxx.sql就能执行非常灵活。3.3 为什么用ScriptUtils而不是自己解析有人可能会说我自己读文件用Statement一行行执行不就行了吗我可以明确告诉你遇到以下情况就会出问题SQL文件里有--注释或/* */注释SQL语句跨了多行脚本里有DELIMITER自定义分隔符的命令脚本里有存储过程、触发器定义里面的分号会被误当成语句结束符ScriptUtils内部做了完整的SQL脚本解析。它会把分号当成语句分隔符同时支持跳过注释行。所以只要你自己不搞特殊语法用它是最稳的。3.4 事务与提交策略这里有个容易忽视的点ScriptUtils.executeSqlScript的默认行为是每条SQL语句单独提交还是统一一个事务看源码会发现executeSqlScript(Connection, Resource)这种方式不会自动提交它复用你传入的连接事务边界由外层控制。当你传入的是一个来自连接池的裸连接很多连接池默认是自动提交模式效果就是每条语句各自提交。如果希望整个SQL文件作为一个事务原子执行可以用DataSourceUtils.getConnection(dataSource)获取被Spring事务管理的连接或者直接在方法上加上Transactional配合TransactionAwareDataSourceProxy。我的建议是大文件不要强行包在一个事务里MySQL在InnoDB下大批量操作会产生很大的undo log而且一旦中间失败全部回滚问题更难定位。值得注意的做法是执行前把脚本里关键语句打点失败时能快速知道是哪一段。4. 企业级方案Flyway集成让SQL变更可追踪4.1 为什么最终要落到迁移工具用脚本来执行很容易但数据库结构是不断演进的。今天加一张表明天加一个字段后天改一个索引。如果团队里每个人都手动执行SQL很快开发库、测试库、生产库的结构就会分叉。Flyway这类迁移工具解决的核心问题用版本号追踪每一次数据库变更保证脚本只执行一次并且按照版本顺序执行。在Spring Boot项目中集成Flyway非常顺滑。4.2 添加依赖与配置dependency groupIdorg.flywaydb/groupId artifactIdflyway-core/artifactId version9.22.3/version /dependencySpring Boot会自动检测到Flyway并在启动时执行迁移。配置如下spring: flyway: enabled: true locations: classpath:db/migration baseline-on-migrate: true validate-on-migrate: true迁移脚本放在src/main/resources/db/migration/下命名规则非常严格V{版本号}__{描述}.sql。V1__create_user_table.sqlV2__add_phone_to_user.sql版本号之间不能重复执行过的脚本会被记录在flyway_schema_history表中。4.3 实际操作中的注意事项我集成Flyway后遇到的第一件事是已经有存量数据库的旧项目第一次启动Flyway时它会直接在flyway_schema_history里插入一条 Flyway Baseline 记录然后从当前版本开始往上执行。这种情况必须设置baseline-on-migrate: true并配合baseline-version指定基线版本。否则Flyway会尝试校验历史表结构跟脚本里的DDL对不上就直接报错。另外一组容易混淆的是validate-on-migrate启动时校验已执行过的脚本是否被改动过如果有人改了历史SQL文件会直接启动失败out-of-order是否允许乱序迁移。默认false严格按版本顺序执行如果团队里有人忍不住去改已经执行过的脚本启用校验后他在本地启动时马上就会看到报错提示这种硬性约束反而是好事。4.4 Flyway与手动执行怎么切换有人会问我已经写了手动执行SQL文件的工具类Flyway是不是多余其实两者定位不同完全可以共存Flyway管结构演进比如建表、加字段所有环境统一手动执行方案管一次性数据处理比如根据某个算法批量修复线上数据这种脚本你不可能放进迁移目录里反复执行我在项目中的做法是结构变更全走Flyway数据修复类的SQL文件通过手动执行接口触发执行前备份。这样既保证了结构统一又保留了操作的灵活性。5. 执行SQL文件时的常见问题与排查实录5.1 SQL文件中文乱码这是最常见的坑尤其是Windows上编辑的SQL文件用UTF-8读取时会乱码。ScriptUtils.executeSqlScript默认使用平台的编码有时候不是UTF-8。解决办法是手动指定编码ScriptUtils.executeSqlScript( connection, resource, EncryptUtils.UTF8, false, false, ScriptUtils.DEFAULT_STATEMENT_SEPARATOR, ScriptUtils.DEFAULT_COMMENT_PREFIX );或者更简单写文件时统一用UTF-8读取时也用UTF-8。我给团队定的规矩是所有SQL文件必须存成UTF-8格式且在文件头写上SET NAMES utf8mb4;MySQL连接串里也要加characterEncodingutf8。5.2 脚本被重复执行启动自动初始化时data.sql是纯INSERT的话第二次启动就会报主键冲突。所以DDL尽量写成CREATE TABLE IF NOT EXISTSDML之前先做幂等判断比如DELETE FROM xxx WHERE ...再插入或者干脆别用自动初始化管DML交给Flyway5.3 Spring Boot版本迁移后配置失效如果从Spring Boot 2.3升级到2.5原来spring.datasource.initialization-mode和spring.datasource.schema/data属性会被移除或忽略。统一改成spring.sql.init.*。有次同事反馈为什么data.sql不执行了查半天发现他把老配置写在了application.yml里Spring Boot 2.7根本不认。这种坑排查起来很费时间升级版本后务必检查一遍数据库初始化的配置项。5.4DELIMITER语法导致执行报错如果你要在SQL文件里写存储过程或者触发器文件内容可能会用到DELIMITER $$这种自定义分隔符。但Spring Boot的脚本初始化机制和ScriptUtils都不支持MySQL的DELIMITER指令因为它不是一个标准SQL语法。我踩过这个坑后选择把存储过程、触发器这类脚本放到Flyway里并把spring.flyway.mysql相关参数配好。如果坚持用ScriptUtils可以考虑在存储过程创建后用代码的方式把整个存储过程文本作为一条SQL提交绕开分隔符问题。5.5 执行顺序schema还是data先跑Spring Boot内置初始化默认先执行schema再执行data这是没问题的。但手动执行时脚本内部如果既有DDL又有DML一定要自己控制顺序。我一般建议写脚本时把建表和插入拆成两个文件通过接口分别执行避免一条大SQL里顺序错乱。5.6 排查工具推荐出问题先别盲改有两条排查路径我常用的开启Spring Boot的SQL日志logging: level: org.springframework.jdbc.datasource.init: DEBUG org.flywaydb: DEBUG这样能看到脚本执行的每一步细节。直接看数据库端是否执行过对应语句比如MySQL的SHOW TABLES、查询flyway_schema_history表能快速判断脚本到底跑没跑。6. 一份可直接参考的落地配置示例最后放一套我最近项目的实际配置组合集成了Spring Boot内置初始化和Flyway按环境做区分。6.1 application-dev.yml开发环境需要每次启动都直接初始化spring: datasource: url: jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver sql: init: mode: always schema-locations: classpath:db/dev/schema.sql >spring: datasource: url: jdbc:mysql://prod-host:3306/demo?useUnicodetruecharacterEncodingutf8 username: prod_user password: ${DB_PASSWORD} sql: init: mode: never flyway: enabled: true locations: classpath:db/migration baseline-on-migrate: true validate-on-migrate: true这里有一个思考过程为什么生产环境禁用sql.init因为生产数据库是资产生命线启动自动初始化一旦脚本有误或者被误改影响面不可控。结构变更必须通过Flyway的历史版本记录来追踪每次变更都有据可查可以回滚。6.3 目录结构推荐这样的目录结构明确区分了初始化脚本和迁移脚本src/main/resources/ ├── db/ │ ├── dev/ │ │ ├── schema.sql │ │ └── data.sql │ └── migration/ │ ├── V1__create_user_table.sql │ ├── V2__add_user_age.sql │ └── V3__create_order_table.sql按照这个约定开发环境启动时先执行db/dev下的schema和dataFlyway负责整个项目所有环境的结构演进并且V1、V2这些迁移脚本在任何环境都只执行一次。个人经验是这套组合在绝大多数Spring Boot项目中都够用结构清晰行为可预期。7. 踩过几次坑之后实打实总结的体会导入SQL文件这个事看起来就是把文件内容跑一遍但实际牵扯到脚本解析、事务控制、执行顺序、环境隔离、版本追踪多个层面。我自己经历过的几次事故都跟方案选型有关一是在生产环境用了自动初始化结果QA给的一份带批量DELETE的初始化脚本把表清了大半二是某次手动执行SQL时脚本里有DELIMITER语法用ScriptUtils直接执行报错排查了半天才发现是分隔符问题三是Spring Boot版本从2.3升到2.7后旧的spring.datasource.schema/data配置静默失效data.sql不跑了最后翻文档才找到新的spring.sql.init.*命名空间。针对这些坑我在项目里定了三条规矩也分享给你参考结构变更DDL一定走Flyway开发、测试、生产环境的脚本版本完全一致。开发环境的自动初始化脚本只用来造本地数据数据文件保持幂等就算被重复执行也不能报错。数据修复类的SQL文件不要塞进迁移目录单独放到一个maintenance/目录执行前必须先备份目标表数据。还有一个小技巧SQL文件引入多个数据源时要特别当心。Spring Boot的spring.sql.init默认只对主数据源生效如果你的项目用了多数据源副数据源想初始化脚本得借助DataSourceInitializer手动配置别指望配置文件里写一行就有效。整体来看Spring Boot导入SQL文件没有一个万能方案每个方案的适用场景不同。自动初始化省事但危险手动执行灵活但需要自己控制细节Flyway工程化程度最高但引入了一定的规范成本。先把方案边界摸清楚再看项目需要什么这样才真的能把导入SQL文件这件事做到位。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑