[Database Migration] 記一次未達預期的資料庫遷移

Elijah_Wang發表於2019-07-30

情景簡介

今日,公司專案迭代開發過程中遇到如下情況:

有一資料表 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 協議》,轉載必須註明作者和本文連結
夏蟲不語冰

相關文章