【问题标题】:Flyway - SQL migration-scripts versions on multiple git branchesFlyway - 多个 git 分支上的 SQL 迁移脚本版本
【发布时间】:2018-05-25 07:45:34
【问题描述】:

在项目中,我们使用 Flyway 迁移工具来开发 db 架构以及这些脚本的 git 历史记录。

想法是每个脚本编号都以数字为前缀,数值增加10,然后是名称,例如:

  • 0000_name_one.sql
  • 0010_name_two.sql
  • 0020_name_three.sql

等等

但是,由于我们必须开始处理多个分支(例如,两个主要分支:ma​​ster + ma​​ster_ext),而且我们还有一些 dev-branches 用于耗时的功能 - 选择正确的脚本编号存在问题。

当长期 dev-branch 引入少量变更脚本时,当将其合并到 ma​​sterma​​ster_ext 时,有人也引入了相同数量的不同脚本我们会发生冲突。 (merge可能会因为名字不同而成功,但是flyway迁移会失败)

在处理多个分支时,是否有任何模式或良好做法来维护 SQL 脚本版本?

提前致谢

【问题讨论】:

    标签: flyway


    【解决方案1】:

    Flyway 不提供任何内置功能来执行相同的操作。

    但是我已经为我的项目做了这个,我认为到目前为止这是我们能做的最好的。

    1. 将 Flyway 版本设置为实际时间戳,因此无论您创建哪个 git 分支,它始终是唯一且有序的。
    2. 编写程序生成此版本并询问所有开发人员 使用此文件以获得下一个版本 需要使用。

    以下是我目前使用的示例

    package com.demo;
    
    
    import java.text.SimpleDateFormat;
    import java.util.Calendar;
    import java.util.Date;
    import java.util.TimeZone;
    
    public class GenerateFileVersion {
    
        public static void main (String... args){
            GenerateFileVersion f=new GenerateFileVersion();
            f.fileVersion();
        }
    
        private String trimOrPad(String str, int length, char padChar) {
            String result;
            if (str == null) {
                result = "";
            } else {
                result = str;
            }
    
            if (result.length() > length) {
                return result.substring(0, length);
            } else {
                while (result.length() < length) {
                    result = padChar+result;
                }
    
                return result;
            }
        }
    
        private String fileVersion(){
            Date date = new Date();
            Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("CST6CDT"));
            calendar.setTime(date);
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat(
                    "yyyy-MM-dd HH:mm:ss");
    
            StringBuffer sb=new StringBuffer();
            sb.append(calendar.get(Calendar.YEAR)%100);
            sb.append(".");
            sb.append(this.trimOrPad(String.valueOf(calendar.get(Calendar.MONTH)+1),2,'0'));
            sb.append(".");
            sb.append(this.trimOrPad(String.valueOf(calendar.get(Calendar.DATE)),2,'0'));
            sb.append(".");
            sb.append(this.trimOrPad(String.valueOf(calendar.get(Calendar.HOUR_OF_DAY)),2,'0'));
            sb.append(this.trimOrPad(String.valueOf(calendar.get(Calendar.MINUTE)),2,'0'));
            sb.append(this.trimOrPad(String.valueOf(calendar.get(Calendar.SECOND)),2,'0'));
    
            System.out.println("Choose Your Next File Name From below list...");
    
            int i=0;
    
            for(ENVIRONMENT env: ENVIRONMENT.values()){
                System.out.println("Next File Name for Making DDL Change : "+"V"+sb.toString()+this.trimOrPad(String.valueOf(i++),2,'0')+"__"+env.toString()+"_DDL.sql");
                System.out.println("Next File Name for Making DML Change : "+"V"+sb.toString()+this.trimOrPad(String.valueOf(i++),2,'0')+"__"+env.toString()+"_DML.sql");
            }
    
            return sb.toString();
        }
    
        private enum ENVIRONMENT{COMMON(1),LOCAL(9),STAGE(4),MTF(5),PERF(7),PROD(2);
            private int value;
            private ENVIRONMENT(int value) { this.value = value; }
        }
    
    
    }
    

    您还可以为您的项目添加更多功能,以确保所有开发人员不会犯任何错误,您可以将以下程序添加为 junit 测试用例,以便在任何文件不遵循 flyway 版本控制标准时构建失败。

    示例如下

    package com.demo;
    
    import junit.framework.Assert;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.reflections.Reflections;
    import org.reflections.scanners.ResourcesScanner;
    import org.reflections.util.ClasspathHelper;
    import org.reflections.util.ConfigurationBuilder;
    import org.reflections.util.FilterBuilder;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    
    import java.util.Collections;
    import java.util.LinkedList;
    import java.util.List;
    import java.util.Set;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class SpringBootRestApplicationTests {
    
        @Test
        public void checkDuplicateVersion() {
    
            System.out.println("Starting checkDuplicateVersion");
    
            List<ClassLoader> classLoadersList = new LinkedList<>();
            classLoadersList.add(ClasspathHelper.contextClassLoader());
            classLoadersList.add(ClasspathHelper.staticClassLoader());
    
            Reflections reflections = new Reflections(new ConfigurationBuilder()
                    .setScanners(new ResourcesScanner())
                    .setUrls(ClasspathHelper.forManifest(ClasspathHelper.forClassLoader(classLoadersList.toArray(new ClassLoader[0]))))
                    .filterInputsBy(new FilterBuilder().include(FilterBuilder.prefix("local"))));
    
            Reflections reflections1 = new Reflections(new ConfigurationBuilder()
                    .setScanners(new ResourcesScanner())
                    .setUrls(ClasspathHelper.forManifest(ClasspathHelper.forClassLoader(classLoadersList.toArray(new ClassLoader[0]))))
                    .filterInputsBy(new FilterBuilder().include(FilterBuilder.prefix("common"))));
    
            Stream<String> stream1=  reflections.getStore().get("ResourcesScanner").keys().stream();
            Stream<String> stream2=  reflections1.getStore().get("ResourcesScanner").keys().stream();
    
            Stream<String> resultingStream=Stream.of(stream1,stream2).flatMap(i -> i);
    
            //resultingStream.forEach(System.out::println);
    
            List<String> existingFileVersions=
                    resultingStream
                            .filter(f -> f.startsWith("V"))
                            .filter(f -> f.endsWith(".sql"))
                            //.forEach(System.out::println);
                            .map(n -> n.split("__")[0].substring(1))
                            //.forEach(System.out::println);
                            .collect(Collectors.toList());
    
            Set<String> duplicateVersion=existingFileVersions.stream().filter(i -> Collections.frequency(existingFileVersions, i) >1)
                    .collect(Collectors.toSet());
    
            duplicateVersion.forEach( i -> System.out.println("Duplicate Version found "+i));
    
            Assert.assertEquals(0,duplicateVersion.size());
        }
    
        @Test
        public void checkFlywayFileNamingStandard(){
    
            System.out.println("Starting checkFlywayFileNamingStandard");
    
            List<ClassLoader> classLoadersList = new LinkedList<>();
            classLoadersList.add(ClasspathHelper.contextClassLoader());
            classLoadersList.add(ClasspathHelper.staticClassLoader());
    
            Reflections reflections = new Reflections(new ConfigurationBuilder()
                    .setScanners(new ResourcesScanner())
                    .setUrls(ClasspathHelper.forManifest(ClasspathHelper.forClassLoader(classLoadersList.toArray(new ClassLoader[0]))))
                    .filterInputsBy(new FilterBuilder().include(FilterBuilder.prefix("local"))));
    
            Reflections reflections1 = new Reflections(new ConfigurationBuilder()
                    .setScanners(new ResourcesScanner())
                    .setUrls(ClasspathHelper.forManifest(ClasspathHelper.forClassLoader(classLoadersList.toArray(new ClassLoader[0]))))
                    .filterInputsBy(new FilterBuilder().include(FilterBuilder.prefix("common"))));
    
            Stream<String> stream1=  reflections.getStore().get("ResourcesScanner").keys().stream();
            Stream<String> stream2=  reflections1.getStore().get("ResourcesScanner").keys().stream();
    
            Stream<String> resultingStream=Stream.of(stream1,stream2).flatMap(i -> i);
            //resultingStream.forEach(System.out::println);
    
            resultingStream
                    .filter(f -> f.endsWith(".sql"))
                    .forEach(n -> {
    
                        if(!n.split("__")[0].toUpperCase().startsWith("V")){
                            System.out.println("File starts with " + n + " Does not start with Letter V or v. Please fix it.");
                            Assert.fail();
                        }
    
                        for(String s : n.split("__")[0].substring(1).split("\\.")){
                            try {
                                //System.out.println(n);
                                Integer.valueOf(s);
                            }catch(Exception e){
                                //e.printStackTrace();
                                System.out.println("File starting with "+ n + " does not match flyway standard");
                                System.out.println("Flyway standard is V{version}__{description}.sql");
                                Assert.fail();
                            }
                        }
                    });
        }
    
    }
    

    更多详情可以看我的项目here

    【讨论】:

      【解决方案2】:

      您不应该认为迁移的版本是 描述 部分开始的“__”(双下划线)之前的部分。

      迁移的命名方案(根据documentation on migrations)是:

      <flag char><version><separator><description><suffix>
      

      (实际上大部分是可配置的,但假设它没有被更改)

      • flag charV(用于版本化迁移)、R(用于可重复迁移、U(用于撤消迁移)
      • version 是任何不使用分隔符字符串的东西。通常会应用一些数字方案。
      • separator_ 是“__”(双下划线)
      • suffix 是为您的迁移定义的任何后缀(例如 .sql

      因为 version 可能是您可以轻松引入特定于开发人员或分支的组件的任何内容,并且通过这种方式将有助于确保名称的唯一性。

      (顺便说一句:根据文档,您在问题中显示的名称不是正确的飞行路径迁移名称!)

      但是

      您仍然面临确保根据逻辑依赖关系有效排序的问题。

      幸运的是,在大多数情况下,排序约束只发生在一个开发单元内,通常是在一个分支内。因此,确保按顺序执行所有相关迁移将给出正确的结果。 要在不同的开发单元中获得有用的排序,您可能只是恢复到时间尺度。

      例如使用一些东西:&lt;yyymmdd&gt;.&lt;unitseq&gt;.&lt;subseq&gt; 在哪里

      • yyyymmdd 是开发单元的日期标记(例如,开发开始或计划部署到生产的日期)
      • unitseq 是标识开发单元的序列号。这需要确保正确排序!因此,使用一组固定的字符会有所帮助。
      • subseq 是一个开发单元内的排序序号。

      对于需要正确排序开发应变的更复杂的案例,您可能仍需要注意跨开发单元的正确排序。

      从问题回到你的例子

      由于您的姓名没有分隔符,因此以下变体仍会被 flyway 假定为不同:

      • 0000_name_one.sql
      • 0000_name_one_projectA.sql
      • 0000_name_one_project_B.sql

      按照我的建议,您可能会转向使用:

      • V20180211.p000.0000__name_one.sql
      • V20180302.p001.0000__name_one_projectA.sql
      • V20180301.p002.0000__name_one_projectB.sql

      请注意,两个版本的执行顺序不同!

      【讨论】:

        猜你喜欢
        • 2020-01-17
        • 2018-02-28
        • 2012-09-24
        • 2011-11-05
        • 2015-06-18
        • 1970-01-01
        • 2020-08-17
        • 1970-01-01
        • 2016-03-12
        相关资源
        最近更新 更多