2
1

Rails でカラムを追加する際、既存レコードに影響を与えずにデフォルト値を設定したい

Last updated at Posted at 2024-06-19

株式会社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

一旦デフォルト値を niladd_column した直後に、change_column_default でデフォルト値を指定する。

マイグレーション後、 Rails コンソールでデータを確認

# レコードが1つ存在する想定
HogeTable.count 
=> 1

# 既存レコードを取得して値を確認する
HogeTable.first
=>
# ....
some_item: nil

# レコードを新規作成して値を確認する
HogeTable.create!
HogeTable.last
=>
# ....
some_item: 100.0

元々存在したレコードの some_itemnil にり、マイグレーション後に作成したレコードの 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)が入る

そのため、前項の通り、一旦デフォルト値を niladd_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

今後作成されるレコードには特定のステータスを割り当て、既存のレコードには「対象外」のステータスを割り当てたい(既存レコードは要件のスコープ外としたい)、といった要件があるかもしれません。

参考記事

今回は、Railsを使用してデフォルト値付きのカラムを追加する際のイレギュラーな対応事例を紹介しました。
この方法で、既存データに影響を与えずにデフォルト値を設定することができます。
どなたかの実装に役立つ機会があれば幸いです。

スペシャルサンクス

本記事中の実装及び執筆中に、弊社フェローの igaigaさん に相談させていただきました。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1