[Database Migration] 记一次未达预期的数据库迁移
情景简介
今日,公司项目迭代开发过程中遇到如下情况:
有一数据表 some_tables
,其中有一字段 some_column
的数据类型为不可为负的 DECIMAL UNSIGNED
,根据业务需求,需要将其设为可以为负的 DECIMAL SIGNED
。
初步尝试
有童鞋要说,这也叫问题吗?好简单的有木有?话不多说,上代码:
public function up()
{
Schema::table('some_tables', function (Blueprint $table) {
$table->decimal('some_column', 8, 2)->nullable(false)->default(0.00)->comment('some comments')->change();
});
}
public function down()
{
Schema::table('some_tables', function (Blueprint $table) {
$table->unsignedDecimal('some_column', 8, 2)->nullable(false)->default(0.01)->comment('some comments')->change();
});
}
蓝后,运行一下 php artisan migrate
不就齐活儿了吗?
然鹅,这并不好用。
运行结果发现,仅有当前字段 some_column
的默认值发生了改变,关键的字段参数 UNSIGNED
值并未置为 FALSE
。
如题目所言:此次数据库迁移,未达预期。
问题分析
问题症结究竟在哪里呢?努力找了一圈,终于可以断言:这个问题应该是 Laravel 框架的锅。
如上代码中,$table->decimal()
与 $table->unsignedDecimal()
两个方法的出处在于:Illuminate\Database\Schema\Blueprint
。源代码如下:
/**
* Create a new decimal column on the table.
*
* @param string $column
* @param int $total
* @param int $places
* @return \Illuminate\Support\Fluent
*/
public function decimal($column, $total = 8, $places = 2)
{
return $this->addColumn('decimal', $column, compact('total', 'places'));
}
/**
* Create a new unsigned decimal column on the table.
*
* @param string $column
* @param int $total
* @param int $places
* @return \Illuminate\Support\Fluent
*/
public function unsignedDecimal($column, $total = 8, $places = 2)
{
return $this->addColumn('decimal', $column, [
'total' => $total, 'places' => $places, 'unsigned' => true,
]);
}
乍一看,两个方法的定义没什么问题,但素,对比类似的 integer()
与 unsigned()
两个方法的定义,就可以看出一些端倪了:
/**
* Create a new integer (4-byte) column on the table.
*
* @param string $column
* @param bool $autoIncrement
* @param bool $unsigned
* @return \Illuminate\Support\Fluent
*/
public function integer($column, $autoIncrement = false, $unsigned = false)
{
return $this->addColumn('integer', $column, compact('autoIncrement', 'unsigned'));
}
/**
* Create a new unsigned integer (4-byte) column on the table.
*
* @param string $column
* @param bool $autoIncrement
* @return \Illuminate\Support\Fluent
*/
public function unsignedInteger($column, $autoIncrement = false)
{
return $this->integer($column, $autoIncrement, true);
}
看到区别了吗,Illuminate\Database\Schema\Blueprint
中关于数据类型 INTEGER
的属性 UNSIGNED
采用的是显式声明,而关于数据类型 DECIMAL
的属性 UNSIGNED
采用的竟然是隐式声明?!
PS: 窃以为,这两部分代码,风格迥异,应该不是出自同一位 Coder 之手。
解决方案
曾经,我天真地以为,将 Blueprint
继承一下,在当前 migration
文件中重新定义该类的 decimal()
和 unsignedDecimal()
两个方法即可。
然鹅,报错了。累觉不爱,无意深究,想到修改 BUG 之最上乘境界便是:修改框架源代码。于是乎:
Location: Illuminate\Database\Schema\Blueprint @ Line 690 ~ 716
/**
* Create a new decimal column on the table.
*
* @param string $column
* @param int $total
* @param int $places
* @param bool $unsigned
* @return \Illuminate\Support\Fluent
*/
public function decimal($column, $total = 8, $places = 2, $unsigned = false)
{
return $this->addColumn('decimal', $column, compact('total', 'places', 'unsigned'));
}
/**
* Create a new unsigned decimal column on the table.
*
* @param string $column
* @param int $total
* @param int $places
* @return \Illuminate\Support\Fluent
*/
public function unsignedDecimal($column, $total = 8, $places = 2)
{
return $this->decimal($column, $total, $places, true);
}
好的,这个问题就酱紫被我很不优雅地解决了。
总结
其实,在开发过程中,貌似这个问题不是很容易遇到的,原因在于:$table->decimal()
方法在创建数据表操作时确实实现了数据类型 DECIMAL SIGNED
的声明,然鹅,问题是,在修改数据表中原数据类型为 DECIMAL UNSIGNED
的字段时,该方法按照原来的定义方式未能显式声明 UNSIGNED
的属性值,因而,默认沿用之前的 UNSIGNED
属性值(TRUE),当且仅当该情况下,童鞋们会发现,今日所述的诡异之坑百分之百重现了。
稍稍总结一下重点:
- 在进行创建数据表操作中,
decimal()
与unsignedDecimal()
皆会如我们所预期分别创建数据类型为DECIMAL SIGNED
和DECIMAL UNSIGNED
字段 - 在进行修改数据表操作中,当被修改字段的原数据类型为
DECIMAL SIGNED
时,unsignedDecimal()
会如我们所预期将该字段的数据类型修改为DECIMAL UNSIGNED
- 在进行修改数据表操作中,当被修改字段的原数据类型为
DECIMAL UNSIGNED
时,decimal()
不会如我们所预期将该字段的数据类型修改为DECIMAL SIGNED
遇到如上第三种情况的童鞋,可以考虑:
- 不优雅如我,粗暴修改框架源码
- 手动修改数据表字段吧
PS: 当然,我也知道,直接修改框架源码实属无奈之举,已计划去 laravel/framework
包的 github
线上仓库 blame
一下,以期造福后人。
补充
其他小数类型 FLOAT & DOUBLE
根据数据类型为 DECIMAL
的字段问题出现的理据推测,当类似的字段修改操作作用于数据类型为 FLOAT
或 DOUBLE
的字段,同样会出现坑点。因为 Blueprint
类中压根就没有定义 unsignedFloat()
方法和 unsignedDouble()
方法,好么!
数据表字段重命名之小 tip
另外,不知道,小伙伴们有没有做过数据表字段重命名的操作,当然,简单的代码示例如下:
public function up()
{
Schema::table('some_tables', function (Blueprint $table) {
$table->renameColumn('some_column', 'another_column');
});
}
public function down()
{
Schema::table('some_tables', function (Blueprint $table) {
$table->renameColumn('another_column', 'some_column');
});
}
然鹅,这并不是重点,这里要分享的重点是,在同一个 migration
文件中,对于同一字段,字段重命名操作与其他修改操作不可以同时存在!
所以,如果需要对某一数据表中的某一字段,进行字段重命名及其他修改操作,请选择将此二种操作分开在两个 migration
文件中执行,先后顺序无关紧要,自己开心就好。
数据表字段追加之小 tip
关于数据表字段追加,当 Database Driver
为 MySQL
时,很多有强迫症倾向的童鞋(eg. 笔者)通常会使用 after('another_column_name')
方法指定该追加字段在数据表中的相对位置。
值得留意的是,这种操作仅在创建数据表和追加数据表字段时有效,在执行数据表字段修改操作时,即使 after('another_column_name')
方法被引入使用,也不会对被修改字段的相对位置产生任何影响。
铭曰:
有技如斯,而不一施;
终不鬻技,其志可悲。
水浅山老,孤坟孰保;
视此铭章,庶几有考。
本作品采用《CC 协议》,转载必须注明作者和本文链接
刚刚,在
github
上收到回复:意即:MySQL 8.x 将弃用
DECIMAL UNSIGNED
:joy:Issue: Change of a Column From DECIMAL UNSIGNED to DECIMAL SIGNED Does Not Work.