1. 问题背景
PHP Laravel 框架中的 db migration 是比较常用的一个功能了. 在每个版本迭代中, 除了代码会变动之外, 一般数据库的字段或者数据库表也会有些变动. 因此在新版本上线时, 除了发布新版代码, 不可避免地要把数据库的变动也执行了. 在没有 db migration 功能之前, 我们的做法是把要变动库表的 SQL 语句写好 (CREATE TABLE,ALTER TABLE 等) 存在一个 sql 文件中, 然后在上线时连接数据库, 将 sql 语句执行一遍.
这么做比较大的一个缺点是没有数据库的版本管理, 万一上线失败, 要回滚版本, 还要把 sql 文件里的内容再写个反向的 SQL(DROP TABLE,DROP COLUMN 等). 这种方式也比较原始, 在 web 开发中, 我们总是希望尽量避免开发直接用原始的 sql 来操作数据库, 出错风险很高, 并且很有可能出现不可逆的错误, 每次操作都要提心吊胆.
于是乎, PHP Laravel 框架提供了 db migration 的功能, 用代码来管理数据库. 参考链接 https://laravel.com/docs/5.7/migrations
2. 问题描述
在一个新的版本中, 我将自己的数据库变更用如下方式记录
php artisan make:migration db_migration_for_new_version
这会在项目的 database/migrations 目录下创建一个新的 PHP 文件, 自己填入要变更的数据库内容
- public function up {
- Schema::create('a_new_table', function(Blueprint $table) {
- $table->bigIncrements('id');
- });
- Schema::create('another_new_table', function(Blueprint $table) {
- $table->bigIncrements('id');
- $table->string('user', 64)->default(0)->comment('用户名');
- // 这里模拟出现错误的情形
- throw new \Exception("出现错误");
- });
- }
在上面这个例子中, 我的本意是想要创建两个表格. 然而在第一个表格创建完了以后, 第二个表格出现错误导致创建失败了. 按照正常流程, 我在上线时应该执行如下指令创建表格
php artisan migrate
由于第二个表格创建失败, 这时候上面的指令必然会报错. 然而报错之后你应该怎么做呢? 首先当然是把代码里出现错误的地方修正, 然后应该怎么搞? 此时数据库里面第一个表已经建好了, 第二个表还没建. 这时候你如果再执行 php artisan migrate 会报错: 你第一张表格已经创建, 不可重复创建表格. 你可能会感觉, 我需要回滚一次, 于是你可能会执行回滚操作 php artisan migrate:rollback --step=1. 这里需要强调, 此时千万别回滚!!!
因为刚才第一次执行 migration 出错, 导致数据库并没有生成一个新的版本号. 这时候如果回滚, 那你回滚的是上个版本发布的时候做执行的数据库操作, 而不是你刚刚执行的这个版本的数据库操作, 这很可能是灾难性的, 会导致你数据丢失. 目前数据库最新版本是什么, 可以参考数据库中 migrations 表的 batch 字段(这个表是 laravel migration 功能自动生成和管理的, 并非业务表).
总结一下这一无解深坑: db migration 进行到一半时出错, 此时只能手动操作数据库把已经执行的操作回滚掉, 无法再通过 artisan 指令进行回滚
3. 为什么无解?
其实 GitHub 和 StackOverflow 上有很多人已经碰到了这个问题, 但是答案都很悲观.
所有人的第一反应都是: 可以开启事务操作么? 将一次 migration 的所有操作视为一个整体, 要么都成功, 要么都失败可以么? 很遗憾, 不支持事务操作. 在 mysql 里面, 只有进行 update,insert,delete 这些常规操作时才可以有事务, 而我们 migration 中执行的都是 DDL(Data Definition Language)操作. 这种建表 (CREATE TABLE), 修改表结构(ALTER TABLE) 的操作是无法回滚的, 即使开启了事务也无法回滚 (参考链接 https://dev.mysql.com/doc/refman/8.0/en/cannot-roll-back.html ). 把 DDL 操作放在一个事务(Transaction) 中, 会导致事务自动的提交(参考链接 https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html ), 这往往不是我们代码逻辑所期望的结果.
4. 那该怎么办?
如果你已经碰到了这种问题, 那没办法只得手动去一条一条看数据库发生了什么变化, 然后自己执行反向操作.
目前只能想到一些预防此问题出现的办法. 根据 GitHub 上的开发者建议, 最好每一个 CREATE TABLE,ALTER TABLE 操作都是一个单独的 migration. 即每次 migration 只建一张表, 或只改一个表结构, 只做一个操作( 参考链接 https://github.com/laravel/framework/issues/302 )......
还有一种办法是, 把自己的建表, 改表操作都放在一个 try catch 结构中, 一旦出现错误, 直接调用 migration 文件中的 down 函数, 把所做的操作回滚掉. 不过这个需要注意 up 和 down 的兼容性. 例如 up 中有 ADD COLUMN 操作, 而 down 中有 DROP COLUMN 操作. 在 ADD COLLUMN 操作执行之前就出错, 直接取执行 down 函数中的 DROP COLUMN, 也会有可能报 COLUMN 不存在的错误.
总之, 这个问题并没有十分完美的解决方案, 堪称无解深坑, 尤其要注意 rollback 操作不要乱做 , 不要为了弥补一个坑, 给自己挖了更大的一个坑.
来源: https://www.qcloud.com/developer/article/1329027