[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 SIGNEDDECIMAL UNSIGNED 字段
  • 在进行修改数据表操作中,当被修改字段的原数据类型为 DECIMAL SIGNED 时,unsignedDecimal() 会如我们所预期将该字段的数据类型修改为 DECIMAL UNSIGNED
  • 在进行修改数据表操作中,当被修改字段的原数据类型为 DECIMAL UNSIGNED 时,decimal() 不会如我们所预期将该字段的数据类型修改为 DECIMAL SIGNED

遇到如上第三种情况的童鞋,可以考虑:

  • 不优雅如我,粗暴修改框架源码
  • 手动修改数据表字段吧

PS: 当然,我也知道,直接修改框架源码实属无奈之举,已计划去 laravel/framework 包的 github 线上仓库 blame 一下,以期造福后人。

补充

其他小数类型 FLOAT & DOUBLE

根据数据类型为 DECIMAL 的字段问题出现的理据推测,当类似的字段修改操作作用于数据类型为 FLOATDOUBLE 的字段,同样会出现坑点。因为 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 DriverMySQL 时,很多有强迫症倾向的童鞋(eg. 笔者)通常会使用 after('another_column_name') 方法指定该追加字段在数据表中的相对位置。

值得留意的是,这种操作仅在创建数据表和追加数据表字段时有效,在执行数据表字段修改操作时,即使 after('another_column_name') 方法被引入使用,也不会对被修改字段的相对位置产生任何影响。

铭曰:
有技如斯,而不一施;
终不鬻技,其志可悲。
水浅山老,孤坟孰保;
视此铭章,庶几有考。

本作品采用《CC 协议》,转载必须注明作者和本文链接
夏蟲不語冰
Elijah_Wang
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 1
Elijah_Wang

刚刚,在 github 上收到回复:

Just a note: in MySQL 8.x unsigned decimal is deprecated and will be removed.

意即:MySQL 8.x 将弃用 DECIMAL UNSIGNED :joy:

Issue: Change of a Column From DECIMAL UNSIGNED to DECIMAL SIGNED Does Not Work.

4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!