Spring Boot如何正确初始化数据库种子数据?仅首次执行且避免重复
解决Spring Boot种子数据重复初始化的方案及后续扩展处理
嘿,我刚好处理过类似的问题,给你几个实用的方案,既能解决当前重复执行data.sql报错的问题,也能轻松应对后续新增种子数据的需求:
一、修改SQL语句:给插入加「安全锁」
最简单的办法就是让数据库只在数据不存在时才执行插入,不同数据库的语法略有不同,核心思路都是判断主键/唯一键是否存在,存在则跳过或不做修改。
针对MySQL的修改示例
把你的data.sql改成这样,用ON DUPLICATE KEY UPDATE来避免重复插入报错:
-- 插入管理员用户,存在则不执行任何操作 INSERT INTO users(id, username, password_hash, email, first_name, last_name) VALUES (1, 'admin', 'comixed', 'email1@domain.com', 'ComixEd', 'Administrator'), (2, 'user', 'comixeduser', 'email2@domain.com', 'ComixEd', 'User') ON DUPLICATE KEY UPDATE id = id; -- 空操作,仅用于避免主键冲突报错 -- 插入角色,存在则跳过 INSERT INTO roles(id, name) VALUES (1, 'Administrator'), (2, 'User') ON DUPLICATE KEY UPDATE id = id; -- 插入用户角色关联,存在则跳过 INSERT INTO users_roles(user_id, role_id) VALUES (1, 1), (1, 2), (2, 2) ON DUPLICATE KEY UPDATE user_id = user_id;
针对PostgreSQL的修改示例
如果用PostgreSQL,换成ON CONFLICT DO NOTHING语法:
INSERT INTO users(id, username, password_hash, email, first_name, last_name) VALUES (1, 'admin', 'comixed', 'email1@domain.com', 'ComixEd', 'Administrator'), (2, 'user', 'comixeduser', 'email2@domain.com', 'ComixEd', 'User') ON CONFLICT (id) DO NOTHING;
二、用Spring Boot初始化器:代码控制更灵活
如果你的种子数据逻辑比较复杂(比如需要关联其他业务数据、做动态判断),用Spring Boot自带的ApplicationRunner或CommandLineRunner会更合适——完全用Java代码控制「数据存在则跳过,不存在则插入」的逻辑。
示例代码
创建一个初始化类,注入你的Repository,在应用启动时执行检查:
@Component public class SeedDataInitializer implements ApplicationRunner { private final UserRepository userRepository; private final RoleRepository roleRepository; private final UserRoleRepository userRoleRepository; // 构造函数注入Repository(替代@Autowired更规范) public SeedDataInitializer(UserRepository userRepository, RoleRepository roleRepository, UserRoleRepository userRoleRepository) { this.userRepository = userRepository; this.roleRepository = roleRepository; this.userRoleRepository = userRoleRepository; } @Override public void run(ApplicationArguments args) throws Exception { initializeRoles(); initializeUsers(); initializeUserRoles(); } private void initializeRoles() { // 检查角色是否存在,不存在则新增 roleRepository.findByName("Administrator") .orElseGet(() -> roleRepository.save(new Role(1L, "Administrator"))); roleRepository.findByName("User") .orElseGet(() -> roleRepository.save(new Role(2L, "User"))); } private void initializeUsers() { userRepository.findByUsername("admin") .orElseGet(() -> userRepository.save(new User(1L, "admin", "comixed", "email1@domain.com", "ComixEd", "Administrator"))); userRepository.findByUsername("user") .orElseGet(() -> userRepository.save(new User(2L, "user", "comixeduser", "email2@domain.com", "ComixEd", "User"))); } private void initializeUserRoles() { Role adminRole = roleRepository.findByName("Administrator").orElseThrow(); Role userRole = roleRepository.findByName("User").orElseThrow(); User adminUser = userRepository.findByUsername("admin").orElseThrow(); User regularUser = userRepository.findByUsername("user").orElseThrow(); // 检查用户角色关联是否存在 userRoleRepository.findByUserIdAndRoleId(adminUser.getId(), adminRole.getId()) .orElseGet(() -> userRoleRepository.save(new UserRole(adminUser.getId(), adminRole.getId()))); userRoleRepository.findByUserIdAndRoleId(adminUser.getId(), userRole.getId()) .orElseGet(() -> userRoleRepository.save(new UserRole(adminUser.getId(), userRole.getId()))); userRoleRepository.findByUserIdAndRoleId(regularUser.getId(), userRole.getId()) .orElseGet(() -> userRoleRepository.save(new UserRole(regularUser.getId(), userRole.getId()))); } }
三、用数据库迁移工具:生产环境首选
如果是生产环境或者需要长期维护的项目,强烈推荐用Flyway或Liquibase这类数据库迁移工具。它们能帮你把所有数据库变更(包括种子数据)版本化管理,启动时只会执行从未运行过的脚本,彻底杜绝重复执行的问题。
用Flyway的快速实现步骤
- 添加Maven依赖
<dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> </dependency>
- 创建版本化迁移脚本
在src/main/resources/db/migration目录下创建脚本,命名遵循Flyway规范:V<版本号>__<描述>.sql
- 初始种子数据脚本:
V1__initial_seed_data.sql(内容就是你原来的SQL,不需要加条件,因为Flyway只会执行一次)
-- insert the administrator INSERT INTO users(id, username, password_hash, email, first_name, last_name) VALUES (1, 'admin', 'comixed', 'email1@domain.com', 'ComixEd', 'Administrator'), (2, 'user', 'comixeduser', 'email2@domain.com', 'ComixEd', 'User'); -- insert the supported roles INSERT INTO roles(id, name) VALUES (1, 'Administrator'), (2, 'User'); -- set the administrator roles INSERT INTO users_roles(user_id, role_id) VALUES (1, 1), (1, 2), (2, 2);
- 后续新增种子数据的处理
比如要新增一个Editor角色,只需要创建新的版本脚本:V2__add_editor_role.sql
-- 新增Editor角色 INSERT INTO roles(id, name) VALUES (3, 'Editor'); -- 给管理员添加Editor角色 INSERT INTO users_roles(user_id, role_id) VALUES (1, 3);
Flyway会自动记录已执行的脚本,启动时只会运行未执行过的新脚本。
三种方案的适用场景对比
| 方案 | 新增数据操作方式 | 适用场景 |
|---|---|---|
| 条件SQL插入 | 在data.sql中添加带条件的INSERT语句 | 小型项目、快速原型 |
| ApplicationRunner | 在初始化类中新增检查和插入逻辑 | 中等复杂度项目,需要灵活逻辑 |
| Flyway/Liquibase | 创建新的版本化迁移脚本 | 生产环境、长期维护的项目 |
内容的提问来源于stack exchange,提问作者mcpierce




