情景簡介
今日,公司專案迭代開發過程中遇到如下情況:
有一資料表 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 協議》,轉載必須註明作者和本文連結