SpringBoot项目启动时自动执行多个SQL脚本

背景

有一个项目fyk-config,该项目需要在配置的时候,需要创建一个配置表(FYK_PROPERTIES),并且向该表中插入各个微服务的配置记录。

解决方案

在SpringBoot中,有一个DataSourceInitializer类,该类会在项目启动的时候,执行初始化脚本。具体代码如下:
首先,在resources目录下,创建文件夹scritp/db,然后在db文件夹下,放入sql文件:
在这里插入图片描述
然后,在项目中,写一个配置类:

@Slf4j
@Configuration
public class DbScriptInit {
    
    

    @Bean
    public DataSourceInitializer dataSourceInitializer(final DataSource dataSource) throws IOException {
    
    
        final DataSourceInitializer initializer = new DataSourceInitializer();
        initializer.setDataSource(dataSource);
        initializer.setDatabasePopulator(this.databasePopulator());
        return initializer;
    }

    /**
     * 初始化数据资源
     * 
     * @author FYK
     * @return org.springframework.core.io.Resource[] 资源对象
     */
    private Resource[] getResources() throws IOException {
    
    
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resolver.getResources("classpath*:script/db/*.sql");
        log.info("加载初始化脚本文件---------start");
        for (Resource resource : resources) {
    
    
            log.info(resource.getFilename());
        }
        log.info("加载初始化脚本文件---------end");
        return resources;
    }

    /**
     * 初始化数据策略
     * 
     * @author FYK
     * @return org.springframework.jdbc.datasource.init.DatabasePopulator 策略对象
     */
    private DatabasePopulator databasePopulator() throws IOException {
    
    
        final ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
        populator.addScripts(this.getResources());
        return populator;
    }
}

注意

有两点需要说明下:

  1. db文件夹下的脚本可能有多个,所以,这里要使用PathMatchingResourcePatternResolver,读取多个.sql的文件;
  2. 获取文件的路径是classpath*:script/db/*.sql,classpath后加个星号,意思是连jar包中的符合该规则的文件,都可以获取到,因为该项目最终会打成jar包来运行,如果不使用这个星号,就会出现在开发的时,没有问题,等到了测试或生产环境,就会出现找不到sql文件问题。当然,这里的文件规则,需要自己定义好,不要扫到了本不应加载的其他包中的文件了。

补充说明

按照上述操作,应该就能完成所需要求了。
但是有以下问题也许需要关注:

  1. SQL脚本是有执行顺序的,例如,在我的项目中,我需要建一个表,然后像该表插入初始化数据,所以有两个脚本,一个是建表,一个是初始化数据插入(当然,如果你所有的SQL语句都在一个文件中,那就不存在这种问题)。那么就应该先执行建表语句,再执行初始化语句。
    给一个解决方案:所有脚本文件的命名规则:序号-表名-操作类型-备注.sql,这里的序号就表示了这些脚本文件的执行顺序。如下:
    在这里插入图片描述
  2. 这些脚本文件,在项目启动的时候,会执行。那么项目重启之后,又会再次执行,这个应该如何避免?
    这里给一个参考:首先为每一个脚本文件,都配置一个验证是否执行的SQL语句,然后在加载这些配置文件的时候,先执行下这个SQL语句,判断是否执行该脚本,代码大致如下:
    在application配置文件中,配置每隔文件对应的验证sql,这里我的每个sql最终都会返回0或者1,返回0表示要执行脚本文件,返回1标识不执行:
fyk.db-script.check-sql={\
  "1-FYK_PROPERTIES-DQL":"select case when exists(select 1 from all_tables t where t.TABLE_NAME = upper('fyk_properties')) then 1 else 0 end as result from dual",\
  "2-FYK_PROPERTIES-DML-fyk-oauth":"select case when exists(select 1 from fyk_properties t where t.application='fyk-oauth') then 1 else 0 end as result from dual"\
  }

然后对DbScriptInit类镜像改造,改造里面的getResources方法:

    @Value("#{${fyk.db-script.check-sql}}")
    private Map<String, String> checkSql;

    @Autowired
    private JdbcTemplate jdbcTemplate;   
     
   /**
     * 初始化数据资源
     * <p>
     * 首先,每个脚本文件,都要对应一个验证sql,只有验证SQL返回0的时候,才执行该脚本文件,否则不执行。
     * </p>
     * 
     * @author FYK
     * @return org.springframework.core.io.Resource[] 资源对象
     */
    private Resource[] getResources() throws IOException {
    
    
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resolver.getResources("classpath*:script/db/*.sql");
        List<Resource> resultList = new LinkedList<>();
        log.info("加载初始化脚本文件---------start");
        String checkSqlStr;
        Integer resutlNum;
        String dqlSqlMatch = "";
        for (Resource resource : resources) {
    
    
            String fileFullName = resource.getFilename();
            // 如果DQL语句都没有执行,则默认要执行DML语句
            if (fileFullName.matches(".*DML.*") && StringUtil.isNotBlank(dqlSqlMatch)
                && fileFullName.matches(dqlSqlMatch)) {
    
    
                resultList.add(resource);
                continue;
            }
            // 获得验证脚本
            checkSqlStr = this.getCheckSql(fileFullName);
            if (StringUtil.isNotBlank(checkSqlStr)) {
    
    
                resutlNum = jdbcTemplate.queryForObject(checkSqlStr, Integer.class);
                if (resutlNum != null && resutlNum == 0) {
    
    
                    resultList.add(resource);
                    if (fileFullName.matches(".*DQL.*")) {
    
    
                        dqlSqlMatch += this.buildDqlSqlMatch(fileFullName, dqlSqlMatch);
                    }
                } else {
    
    
                    log.info("sql初始化脚本文件[{}]验证结果为1,跳过该脚本", fileFullName);
                }
            }
        }
        log.info("加载初始化脚本文件---------end");
        return resultList.toArray(new Resource[0]);
    }

    /**
     * 获取去掉后缀的文件名
     * 
     * @author FYK
     * @param fileFullName
     *            资源文件全名
     * @return java.lang.String 文件名
     */
    private String getCheckSql(@NonNull String fileFullName) {
    
    
        String fileName = fileFullName.replace(".sql", "");
        log.info("{}.sql", fileName);
        String checkSqlStr = checkSql.get(fileName);
        if (StringUtil.isBlank(checkSqlStr)) {
    
    
            log.warn("sql脚本文件[{}.sql]的执行验证语句未配置~~~~~不执行该SQL脚本", fileName);
        }
        return checkSqlStr;
    }

    /**
     * 构建DQL语句匹配规则
     * 
     * @author FYK
     * @param fileFullName
     *            全文件名
     * @param dqlSqlMatch
     *            DQL语句匹配规则
     * @return java.lang.String 构建结果
     */
    private String buildDqlSqlMatch(String fileFullName, String dqlSqlMatch) {
    
    
        String[] fileFullNameSplit = fileFullName.split("-");
        StringBuilder sb = new StringBuilder();
        if (StringUtil.isBlank(dqlSqlMatch)) {
    
    
            sb.append(".*\\-");
        } else {
    
    
            sb.append("|.*\\-");
        }
        sb.append(fileFullNameSplit[1]);
        sb.append("\\-.*");
        return sb.toString();
    }

仅供参考!!!

猜你喜欢

转载自blog.csdn.net/fyk844645164/article/details/107893626