You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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自带的ApplicationRunnerCommandLineRunner会更合适——完全用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())));
    }
}

三、用数据库迁移工具:生产环境首选

如果是生产环境或者需要长期维护的项目,强烈推荐用FlywayLiquibase这类数据库迁移工具。它们能帮你把所有数据库变更(包括种子数据)版本化管理,启动时只会执行从未运行过的脚本,彻底杜绝重复执行的问题。

用Flyway的快速实现步骤

  1. 添加Maven依赖
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>
  1. 创建版本化迁移脚本
    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);
  1. 后续新增种子数据的处理
    比如要新增一个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

火山引擎 最新活动