Spring Boot 首次启动时数据初始化的艺术:掌握ApplicationRunner与Flyway的结合

362 阅读5分钟

在开发企业级应用的时候,确保应用启动时数据的完整性和一致性绝对是件大事。这就好比给房子打地基,地基不稳,房子迟早会塌。今天,聊聊如何借助Spring Boot和Flyway的组合拳,优雅地搞定这个问题。

在处理大规模数据集时,通常可以使用Flyway来管理和维护这些数据。然而,当需要对Flyway已管理的数据进行更新或修改时,通过添加新的Flyway迁移脚本来实现这一目标可能会显得不够高效。在这种情况下,一种更为灵活的方法是在Spring Boot应用程序启动过程中执行特定的数据初始化或更新逻辑。

此外,对于那些只需要一次性执行的任务,类似于Flyway的迁移机制,但仅限于首次部署时运行且后续重启时不重复执行的需求,可以通过Flyway 的回调机制保证任务按照预期执行一次,又能避免不必要的重复操作。

Spring Boot 的 ApplicationRunner:启动时的“小助手”

首先说说Spring Boot的ApplicationRunner。这个接口就像是一个“启动管家”,它允许我们在应用启动完成后执行一些自定义任务。比如,我们可以用它来检查数据库是否正常、初始化一些默认数据,或者做一些必要的配置。

举个例子,假设我们有一个模板服务,需要在应用启动时检查数据库里是否存在某些默认模板。如果不存在,就自动创建它们。

@Slf4j
@Component
@Order(1)
public class InitDataRunner implements ApplicationRunner {

    @Resource
    private TemplateService templateService;

    @Override
    public void run(ApplicationArguments args) {
        // 初始化模板
        initTemplate("Template", "模板", 
            template -> template.setFlag(1),
            "初始化模板 失败{}"
        );
        addTemplateIfNotExists("xxx", this::initTemplate, "新增模板", "新增模板失败{}");

    }    
    /**
     * 如果没有找到对应的模板,执行updater逻辑,创建新的模板并保存到数据库中。
     * 如果有多个模板匹配,选择更新第一条模板。
     */
    private void initTemplate(String templateCode, String logMessage, Consumer<Template> updater, String errorLog) {
        try {
            log.info("初始化模板:{}", logMessage);
            List<Template> templates = templateService.lambdaQuery()
                    .eq(Template::getTemplateCode, templateCode)
                    // 根据需求调整查询条件
                    .list();
            if (templates.isEmpty()) {
                log.info("未找到符合条件的模板");
            } else if (templates.size() == 1) {
                updater.accept(templates.get(0));
                templateService.updateById(templates.get(0));
            } else {
                log.warn("发现多条符合条件的模板,数量:{}", templates.size());
                // 更新第一条数据
                updater.accept(templates.get(0));
                templateService.updateById(templates.get(0));
            }
        } catch (Exception e) {
            log.error(errorLog, e);
        }
    }
    /**
     * 如果模板不存在,则新增模板
     *
     * @param templateCode 模板代码
     * @param initMethod   初始化方法
     * @param successLog   成功日志信息
     * @param errorLog     错误日志信息
     */
    private void addTemplateIfNotExists(String templateCode, Runnable initMethod, String successLog, String errorLog) {
        try {
            Template template = templateService.lambdaQuery()
            .eq(Template::getTemplateCode, templateCode)
            .one();
            if (ObjectUtils.isEmpty(template)) {
                initMethod.run();
                log.info(successLog);
            }
        } catch (Exception e) {
            log.error(errorLog, e);
        }
    }
    /**
     * 初始化模板
     */
    private void initTemplate() {
        try {
            Template template = createTemplate(xxx);
            templateService.save(template);
        } catch (Exception e) {
            log.error("保存失败: {}", e.getMessage()); 
        }
    }

这段代码的核心逻辑其实很简单:检查数据库中是否存在某个模板,如果不存在就创建它;如果存在多个模板,则更新第一条记录。这种逻辑在实际开发中很常见,比如在系统首次启动时,我们需要确保某些默认配置已经存在。

Flyway 的回调:让初始化任务只执行一次

接着,再来说说Flyway。Flyway是一个非常强大的数据库迁移工具,它可以帮我们管理数据库的版本和结构。不过,有时候我们不仅仅需要管理表结构,还需要在数据库中插入一些默认数据。比如,当我们部署一个新的应用版本时,可能需要在数据库中添加一些新功能所需的初始数据。

但是问题来了:如果我们直接用Flyway脚本来插入数据,每次应用重启时都会重复执行这些脚本,这显然不是我们想要的。那么,怎么才能确保某些初始化任务只在应用首次启动时执行一次呢?

答案就是Flyway的回调机制。通过实现Flyway的Callback接口,我们可以监听数据库迁移的事件,并在特定时刻执行自定义逻辑。比如,我们可以监听AFTER_MIGRATE事件,在所有迁移脚本执行完毕后,插入一些默认数据。

下面是一个简单的例子:

/**
 * 只在部署之后执行一次
 */
@Slf4j
@Configuration
public class InitOutWorkProductData {

    @Resource
    private OAuthClientDetailsService oauthClientDetailsService;
    @Bean
    public Callback callback () {

        return new  Callback () {
            // 判断是否初次启动
            private boolean isFirst = true;

            @Override
            public boolean supports(Event event, Context context) {
                return event == Event.BEFORE_MIGRATE || event == Event.AFTER_MIGRATE;
            }

            @Override
            public boolean canHandleInTransaction(Event event, Context context) {
                return false;
            }

            @Override
            public void handle(Event event, Context context) {
                if (!isFirst) {
                    return;
                }
                // 判断flyway记录表是否存在  Event.BEFORE_MIGRATE  flyway的前置事件
                if (event == Event.BEFORE_MIGRATE) {
                    try (Database database = DatabaseFactory.createDatabase(context.getConfiguration(), false)) {
                        Schema currentSchema = database.getMainConnection().getCurrentSchema();
                        Table table = currentSchema.getTable(context.getConfiguration().getTable());
                        isFirst = !table.exists();
                    }
                }
                // Event.BEFORE_MIGRATE  flyway的后置事件
                if (event == Event.AFTER_MIGRATE) {
                    isFirst = false;
                    // flyway完成之后执行一次
                    try {
                        //具体逻辑
                    }
                }

            }
        };
    }

在这个例子中,我们通过监听BEFORE_MIGRATEAFTER_MIGRATE事件,确保初始化任务只在应用首次启动时执行一次。isFirst这个布尔变量就是用来标记是否是第一次启动的。

实际应用中的权衡

当然,选择哪种方式取决于具体的业务需求和团队的技术偏好。如果团队对Flyway脚本的管理已经非常成熟,可能更倾向于继续使用Flyway来处理所有数据相关的改动。但如果改动较为频繁或涉及动态逻辑,直接在Spring Boot中实现可能会更加高效。

我个人的经验是,在处理小规模或动态的数据更新时,直接在Spring Boot中实现通常更方便。而对于大规模的数据结构调整或初始化任务,Flyway依然是首选。此外,对于那些只需要执行一次的任务,结合Flyway的回调机制往往是最稳妥的选择,因为它与数据库版本控制紧密结合,能够更好地保证任务的执行顺序和一致性。

总之,无论是选择Flyway还是Spring Boot的方式,关键在于根据具体场景做出合理的选择。希望这篇文章能为你提供一些思路!