株式会社iCAREの西口です。
今回は、デフォルト値付きでカラムを追加する要件のレアケースに遭遇したので記事に残します。
実現したかったこと
デフォルト値を指定してカラムを追加する、という実装はよくあると思いますが、今回は下記要件を満たす必要がありました。
- 既に存在するレコードにはデフォルト値を適用せず、
nil
を割り当てたい - 今後作成するレコードにはデフォルト値が入るようにしたい
結論から 💁♂️
うまくいったコード
class AddColumnToHogeTables < ActiveRecord::Migration[5.0]
def change
# デフォルト値を nil でカラムを追加した後、 100.0 に設定する
add_column :hoge_tables, :some_item, :float, default: nil
change_column_default :hoge_tables, :some_item, from: nil, to: 100.0
end
end
一旦デフォルト値を nil
で add_column
した直後に、change_column_default
でデフォルト値を指定する。
マイグレーション後、 Rails コンソールでデータを確認
# レコードが1つ存在する想定
HogeTable.count
=> 1
# 既存レコードを取得して値を確認する
HogeTable.first
=>
# ....
some_item: nil
# レコードを新規作成して値を確認する
HogeTable.create!
HogeTable.last
=>
# ....
some_item: 100.0
元々存在したレコードの some_item
は nil
にり、マイグレーション後に作成したレコードの some_item
にはデフォルト値が入っていることを確認。
うまくいかなかったコード
class AddColumnToHogeTables < ActiveRecord::Migration[5.0]
def change
# 普通に書いたらこう
add_column :hoge_tables, :some_item, :float, default: 100.0
end
end
今回の要件である「今後作成するレコードにはデフォルト値が入るようにしたい」 は満たせるのですが、「既に存在するレコードにはデフォルト値を適用せず、 nil
を割り当てたい」は満たせませんでした。
HogeTable.first
=>
some_item: 100.0 # 既存レコードには、 nil ではなくデフォルト値(100.0)が入ってしまう
HogeTable.create!
HogeTable.last
=>
some_item: 100.0 # 新規レコードには、意図通りデフォルト値(100.0)が入る
そのため、前項の通り、一旦デフォルト値を nil
で add_column
した直後に、change_column_default
でデフォルト値を指定する方法で要件を満たせることがわかりました。
今回試したすべてのコード
他にもいくつか試したので、記録しておきます。
マイグレーションファイル
class AddColumnsToHogeTables < ActiveRecord::Migration[5.0]
def change
# 何も指定しない
add_column :hoge_tables, :case_1_column, :float
# デフォルト値のみ設定
add_column :hoge_tables, :case_2_column, :float, default: 100.0
# NOT NULL制約とデフォルト値を設定
add_column :hoge_tables, :case_3_column, :float, default: 100.0, null: false
# デフォルト値を NULL でカラムを追加した後、 100.0 に設定する
add_column :hoge_tables, :case_4_column, :float, default: nil
change_column_default :hoge_tables, :case_4_column, from: nil, to: 100.0
end
end
コンソールでのデータ確認
# 既存レコードのレコードを取得して値を確認する
HogeTable.first
=>
case_1_column: nil,
case_2_column: 100.0,
case_3_column: 100.0,
case_4_column: nil # 既存レコードには、意図通り nil が入る
# レコードを新規作成して値を確認する
HogeTable.create!
HogeTable.last
=>
case_1_column: nil,
case_2_column: 100.0,
case_3_column: 100.0,
case_4_column: 100.0 # 新規レコードには、意図通りデフォルト値(100.0)が入る
Rubocop の BulkChangeTable に引っかかる場合
カラムを複数追加する際に add_column
メソッドで書いた結果、 Rubocop に bulk オプションを使うように指摘されました。
そのため、以下のように bulk: true
オプションを使って対応しました。
class AddColumnsToHogeTables < ActiveRecord::Migration[5.0]
def change
# カラム追加
change_table :hoge_tables, bulk: true do |t|
t.float :first_item, default: nil
t.float :second_item, default: nil
t.float :third_item, default: nil
t.float :fourth_item, default: nil
end
# デフォルト値設定
change_table :hoge_tables, bulk: true do |t|
t.change_default :first_item, from: nil, to: 5.0
t.change_default :second_item, from: nil, to: 10.0
t.change_default :third_item, from: nil, to: 50.0
t.change_default :fourth_item, from: nil, to: 100.0
end
end
end
bluk: true
オプションを使うことで、追加するカラムの数に関わらず発行される ALTER 文が2つで済みます。
ALTER TABLE "hoge_tables" ADD "first_item" float DEFAULT NULL, ADD "second_item" float DEFAULT NULL, ADD "third_item" float DEFAULT NULL, ADD "fourth_item" float DEFAULT NULL;
ALTER TABLE "hoge_tables" ALTER COLUMN "first_item" SET DEFAULT 5.0, ALTER COLUMN "second_item" SET DEFAULT 10.0, ALTER COLUMN "third_item" SET DEFAULT 50.0, ALTER COLUMN "fourth_item" SET DEFAULT 100.0;
bluk: true
オプションを使わないと、追加するカラムの数 × 2 の ALTER 文が発行され DB への負荷が高くなってしまいます。
class AddColumnToHogeTables < ActiveRecord::Migration[5.0]
def change
add_column :hoge_tables, :first_item, :float, default: nil
change_column_default :hoge_tables, :first_item, from: nil, to: 5.0
add_column :hoge_tables, :second_item, :float, default: nil
change_column_default :hoge_tables, :second_item, from: nil, to: 10.0
add_column :hoge_tables, :third_item, :float, default: nil
change_column_default :hoge_tables, :third_item, from: nil, to: 50.0
add_column :hoge_tables, :fourth_item, :float, default: nil
change_column_default :hoge_tables, :fourth_item, from: nil, to: 100.0
end
end
ALTER TABLE "hoge_tables" ADD "first_item" float DEFAULT NULL;
ALTER TABLE "hoge_tables" ALTER COLUMN "first_item" SET DEFAULT 5.0;
ALTER TABLE "hoge_tables" ADD "second_item" float DEFAULT NULL;
ALTER TABLE "hoge_tables" ALTER COLUMN "second_item" SET DEFAULT 10.0;
ALTER TABLE "hoge_tables" ADD "third_item" float DEFAULT NULL;
ALTER TABLE "hoge_tables" ALTER COLUMN "third_item" SET DEFAULT 50.0;
ALTER TABLE "hoge_tables" ADD "fourth_item" float DEFAULT NULL;
ALTER TABLE "hoge_tables" ALTER COLUMN "fourth_item" SET DEFAULT 100.0;
参考: https://www.rubydoc.info/gems/rubocop/0.61.1/RuboCop/Cop/Rails/BulkChangeTable
要件の背景(おまけ)
詳細は記載できませんが…
1️⃣ とあるテーブルに、デフォルト値付きでカラムを追加したい。
2️⃣ ただ、新しいカラムといえど既に提供している顧客データには変更を加えたくない。
3️⃣ よって、既に存在するレコードの新カラムには nil を割り当て、今後作成するレコードの新カラムにはデフォルト値が入るようにしたい。
4️⃣ しかし、Rails でデフォルト値を指定してマイグレーションすると既存のデータにもデフォルト値が入ってしまうため、要件を満たせない。
「どのようにマイグレーションすれば実現できるんだ…?」
と、パッと浮かばなかったので色々試しました、というのが背景です。
+α
その他のユースケースとして、enum でステータスを設定する際にも今回の方法が使えると思います。
class AddColumnToPiyoTables < ActiveRecord::Migration[5.0]
def change
add_column :piyo_tables, :status, :integer, default: 0 # 「対象外」のステータス
change_column_default :piyo_tables, :status, from: 0, to: 10
end
end
今後作成されるレコードには特定のステータスを割り当て、既存のレコードには「対象外」のステータスを割り当てたい(既存レコードは要件のスコープ外としたい)、といった要件があるかもしれません。
参考記事
- https://api.rubyonrails.org/v7.1/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-change_column_default
- https://api.rubyonrails.org/v7.1/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-change_table
〆
今回は、Railsを使用してデフォルト値付きのカラムを追加する際のイレギュラーな対応事例を紹介しました。
この方法で、既存データに影響を与えずにデフォルト値を設定することができます。
どなたかの実装に役立つ機会があれば幸いです。
スペシャルサンクス
本記事中の実装及び執筆中に、弊社フェローの igaigaさん に相談させていただきました。