数据库迁移框架Flyway介绍

官方文档

https://flywaydb.org/getstarted/firststeps/api[https://flywaydb.org/getstarted/firststeps/api]

入门示例

Java代码

package foobar;

import org.flywaydb.core.Flyway;

public class App {
    public static void main(String[] args) {
        Flyway flyway = new Flyway();
        // 指定数据源
        flyway.setDataSource("jdbc:mysql://localhost/test", "root", "root");
        // 开始数据迁移
        flyway.migrate();
    }
}

在classpath下添加SQL文件 db/migration/V1__Create_person_table.sql

create table PERSON (
    ID int not null,
    NAME varchar(100) not null
);

运行程序,在test数据库会自动创建PERSON表。

后续新增表和字段,只需在db/migration目录下新增SQL文件,格式为V${version}__${name}.sql,version值依次增加,比如V2__name.sql,V3__name.sql。

原理介绍

Flyway的数据库迁移的实现原理是,从classpath或文件系统中找到符合规则的数据库迁移脚本,比如db/migration目录下命名规则为V${version}__${name}.sql的文件,将脚本按照version进行排序,依次执行。执行过的脚本会作为一条记录,存储在schema_version表中。当下次执行迁移时,判断脚本已经执行,则跳过。

MigrationResolver接口负责查找数据库迁移脚本,方法为resolveMigrations(),数据库迁移脚本用ResolvedMigration对象表示。MigrationResolver包含多种实现类,比如SqlMigrationResolver会从classpath下查找sql文件。查询通过Scanner类实现,Location类指定查询路径,sql文件的命名规则需要符合V${version}__${name}.sql,规则中的前缀V、后缀.sql、分隔符__均在FlywayConfiguration接口中定义。另一种实现类JdbcMigrationResolver会从classpath下查找实现JdbcMigration接口的类,类的命名规则需要符合V${version}__${name}。需要扩展自己的实现类,可以继承BaseMigrationResolver。

MetaDataTable接口负责查找已执行的数据库迁移脚本,方法为findAppliedMigrations(),已执行的数据库迁移脚本用AppliedMigration对象表示。MetaDataTable只有一种实现类MetaDataTableImpl,从数据库schema_version表查询所有记录。

ResolvedMigration集合包含了已经执行的AppliedMigration集合,在执行ResolvedMigration前,需要对比AppliedMigration,找到已执行和未执行的ResolvedMigration,对比通过MigrationInfoServiceImpl.refresh()实现。已执行的ResolvedMigration需要校验文件有没有发生变化,有变更则提示错误。未执行的ResolvedMigration依次执行,执行结果记录在schema_version表中。

Flyway的主要方法:

public class Flyway {
   /**数据库迁移*/
   public int migrate();
   /**校验已执行的迁移操作的变更情况*/
   public void validate();
   /**清理数据库*/
   public void clean();
   /**设置数据库的基准版本*/
   public void baseline();
   /**删除执行错误的迁移记录*/
   public void repair();
   /**准备执行环境,并执行Command操作,以上方法都调用了execute()来执行操作*/
   <T> T execute(Command<T> command);
}

接下来我们分析Flyway.migrate()代码执行的主逻辑。

public void migrate() {
  //由Flyway.execute()准备Command.execute()执行所需要的参数
  return execute(new Command<Integer>() {
    public Integer execute(Connection connectionMetaDataTable,
      MigrationResolver migrationResolver,  MetaDataTable metaDataTable, 
      DbSupport dbSupport, Schema[] schemas, FlywayCallback[] flywayCallbacks) {
      //为了简化代码,忽略参数传递
      doMigrate();

    }
  });
}

private void doMigrate() {
  //校验已执行的迁移操作的变更情况
  if (validateOnMigrate) {
    doValidate(connectionMetaDataTable, dbSupport, migrationResolver,
      metaDataTable, schemas, flywayCallbacks, true);
  }
  
  //如果尚未进行数据迁移,即schema_version表中不存在数据,
  //并且数据库不为空,则插入一条baseline信息
  if (!metaDataTable.exists()) {
    //数据库不为空
    if (!nonEmptySchemas.isEmpty()) {
      //插入一条baseline信息
      new DbBaseline(connectionMetaDataTable, dbSupport, metaDataTable, 
      schemas[0], baselineVersion, baselineDescription, flywayCallbacks).baseline();
    }
  }
  
  //进行数据迁移
  DbMigrate dbMigrate = new DbMigrate(connectionUserObjects, dbSupport, 
    metaDataTable,schemas[0], migrationResolver, ignoreFailedFutureMigration, 
    Flyway.this);
  return dbMigrate.migrate();
}

接下来看DbMigrate.migrate()的代码片段。

MigrationInfoServiceImpl对比ResolvedMigration和AppliedMigration对象,找出需要执行数据库迁移脚本,通过pending()方法返回。最后执行数据库迁移脚本。

public int migrate() {
  int migrationSuccessCount = 0;
  while (true) {
    int count = metaDataTable.lock(new Callable<Integer>() {
                
      @Override
      public Integer call() {
        //为了简化代码,忽略参数传递
        return doMigrate();
      }
    }
    if (count == 0) {
      // No further migrations available
      break;
    }
    migrationSuccessCount += count;
  }
  return migrationSuccessCount;
}

private int doMigrate() {
  //收集已经入库的数据库迁移记录,和以文件形式存在的数据库迁移脚本
  MigrationInfoServiceImpl infoService = new MigrationInfoServiceImpl(
    migrationResolver, metaDataTable, configuration.getTarget(), 
    configuration.isOutOfOrder(), true, true, true);
  infoService.refresh();
  
  //infoService.pending()记录将要执行的数据库迁移脚本
  LinkedHashMap<MigrationInfoImpl, Boolean> group = 
    groupMigrationInfo(infoService.pending());
  if (!group.isEmpty()) {
    //执行数据库迁移操作
    applyMigrations(group);
  }
}

DbMigrate.doMigrateGroup() 执行数据库迁移脚本

private void doMigrateGroup() {
  //执行迁移脚本
  migration.getResolvedMigration().getExecutor().execute(connectionUserObjects);
  
  //存入数据库
  AppliedMigration appliedMigration = new AppliedMigration(migration.getVersion(), 
    migration.getDescription(), migration.getType(), migration.getScript(), 
    migration.getResolvedMigration().getChecksum(), executionTime, true);
  metaDataTable.addAppliedMigration(appliedMigration);
}

扩展练习

在一个已经上线的游戏项目中引入Flyway框架,对于已经存在的游戏服,只执行新增的sql语句,对于新搭建的游戏服,需要创建数据库,并执行新增的sql语句。

在这个需求中,需要做的是对于新搭建的游戏服,需要创建数据库。我们可以通过FlywayCallback实现这一点,如果指定数据库为空,则执行初始化数据库的语句。

代码如下:

public static void main(String[] args) {
    final Flyway flyway = new Flyway();
    flyway.setDataSource("jdbc:mysql://localhost/test", "root", "root");
    
    FlywayCallback flywayCallback = new BaseFlywayCallback() {
        
        @Override
        public void beforeMigrate(Connection connection) {
            DbSupport dbSupport = DbSupportFactory.createDbSupport(connection, false);
            if(!hasTable(dbSupport)) {
                initDb(dbSupport);
            }
        }

        private boolean hasTable(DbSupport dbSupport) {
            Schema<?> schema = dbSupport.getOriginalSchema();
            Table[] tables = schema.allTables();
            if(tables == null || tables.length == 0) {
                return false;
            }
            //忽略表 schema_version
            if(tables.length == 1 && 
                tables[0].getName().equalsIgnoreCase("schema_version")) {
                return false;
            }
            return true;
        }
        

        private  void initDb(DbSupport dbSupport) {
            Scanner scanner = new Scanner(
              Thread.currentThread().getContextClassLoader());
            Resource[] resources = scanner.scanForResources(
              new Location("db/init"), "", ".sql");
            if(resources == null || resources.length == 0) {
                throw new RuntimeException(
                  "db/init/*.sql not found in the classpath. ");
            }
            for(Resource resource : resources) {
              SqlMigrationExecutor executor = new SqlMigrationExecutor(
                  dbSupport, resource, PlaceholderReplacer.NO_PLACEHOLDERS, flyway);
              executor.execute(dbSupport.getJdbcTemplate().getConnection());
            }
        }
    };
    
    List<FlywayCallback> callbacks = new ArrayList<FlywayCallback>(
      Arrays.asList(flyway.getCallbacks()));
    callbacks.add(flywayCallback);
    flyway.setCallbacks(callbacks.toArray(new FlywayCallback[callbacks.size()]));
    
    flyway.setBaselineOnMigrate(true);
    flyway.repair();
    flyway.migrate();
    
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,158评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,600评论 1 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,785评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,655评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,075评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,002评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,146评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,918评论 0 211
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,671评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,838评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,318评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,636评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,343评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,187评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,982评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,126评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,934评论 2 279

推荐阅读更多精彩内容