NULL禁止制約を後付けする際の注意点

データメンテについて

すでにあるカラムをNULL禁止にしようとする際の注意点を教えて頂いたので、以下にまとめます。

参考コード1

まず最初に記述したコード

class ChangeColumnToAllowNull < ActiveRecord::Migration[6.1]
  def up
    change_column :comments, :user_id, :integer, null: true
  end

  def down
    change_column :comments, :user_id, :integer, null: false
  end
end

null: falseに変更しようとしています。
以下指摘

down のときのようにあとから NULL 禁止制約をつける場合、対象カラムが NULL であるレコードがすでに存在しているとマイグレーション適用時にエラーになってしまいます。
そして、本番運用しているサービスだと大抵の場合はそういうデータが存在しているものなので、制約をつける前に、何かしらのデータを入れる必要があります(これをデータメンテと呼んだりします)。今回の場合はこんな感じになります。

def down
  # id: 1 のユーザーに無理やり関連付ける
  execute "UPDATE comments SET user_id = 1 WHERE user_id IS NULL"
  change_column :comments, :user_id, :integer, null: false
end

例として id: 1 のユーザーに無理やり関連付けていますが、これはあくまで一例です。
すでに運用しているサービスの場合には、チームで話し合ってもうちょっと現実的な手段を取ります。

ご指摘の通り本番サービスを意識すると、こういったそもそものデータ基盤自体の変更って気軽にできませんので、慎重に検討すべき事由です。

参考コード2

またカラムの変更方法は、下記の様にも書けます。

api.rubyonrails.org

変更したコードが下記。
だいぶスッキリ!

class ChangeColumnToAllowNull < ActiveRecord::Migration[6.1]
  def change
    change_column_null(:comments, :user_id, true)
  end
end

そして下記が、さらに本番を意識した記述方法。

class ChangeColumnToAllowNull < ActiveRecord::Migration[6.1]
  def change
    # 実装当時のチーム協議の結果、ロールバック時は、退会済みユーザのコメントはダミーのユーザアカウントを用意してそれに関連づけてしまう
    # 協議ログ: https://hogehoge-log.com/xxxxxx
    change_column_null(:comments, :user_id, true, 2)
  end
end

詳細

change_column_null は、NULL 許容 -> 禁止 に変更する場合、NULL になっているカラムを何の値で埋めるかをオプションで受け付けてくれます。
なのでそれを使う感じですね〜
あと、後にこのマイグレーションファイルを見た人が「なぜ2で埋めるのか」がわかるように詳しくコメントを書いています(これはプラクティスなので良いですが、実際運用されているアプリケーションでは、時間が経ってコンテキストが失われても読めばわかる状態にすることが本当に重要です)。

この書き方だと、楽に書けるのでいいですね。
そしてちゃんと起こりうるエラーケースへの対策も想定した書き方もできます。
うーんやっぱりRailsは知るほど楽しい。

最後に

こういった思いやりや運用方針があるかで、その後の保守のやりやすさが段違いでしょう。
ラクティス中でも、実際の業務を意識したアドバイスを頂けるのはありがたいです。
以上です。