概要
Rails で DB の構成管理に ridgepole を使っているんだけれど、 MySQL の TEXT 型に後から Not Null 制約を付けるとエラーになってしまう。
github.com
原因
例えば下記の用に定義していたとする。
t.text "my_text", null: true, comment: "テキスト"
しかし null: true
にして、実行した場合には最終的に default: nil
が不正になって、エラーが発生してしまう。
t.text "my_text", null: false, comment: "テキスト"
-- change_column("my_table", "my_text", :text, {:null=>false, :comment=>"テキスト", :default=>nil, :unsigned=>false})
[ERROR] Mysql2::Error: Invalid use of NULL value
では default 値を指定すればいいのかというと、実は MySQL の TEXT 型は default 値を設定できないため、エラーになる。
t.text "my_text", null: false, default: "dummy", comment: "テキスト"
-- change_column("my_table", "my_text", :text, {:null=>false, :comment=>"テキスト", :default=>"dummy", :unsigned=>false})
[ERROR] Mysql2::Error: BLOB, TEXT, GEOMETRY or JSON column 'description' can't have a default value
対応
スマートにやる方法は浮かばなかったので、別名カラムを用意して、最終的に元の名前にリネームするという手段を取ることにした。
データの移行が必要だったりするので、レコード数や値によっては大変かもしれない。
t.text "my_text", null: true, comment: "テキスト"
t.text "my_text_tmp", null: false, comment: "テキスト"
移行先のカラムにデータ移行した後、必要であれば Rails の alias_attribute
を使って、移行先で運用に問題ないか確認できる。
https://railsdoc.com/page/alias_attribute
alias_attribute :my_text, :my_text_tmp
使っているライブラリによっては alias_attribute
だと二重参照になるような場合もある。
自前でメソッドを作って回避する方法もあるが、逆に別名が局所的になるので、一長一短ではある。
def my_text
my_text_tmp
end
問題なさそうなら、古いカラムを別名にする。
カラム名を元に戻すタイミングとアプリケーションの反映タイミングがずれるとエラーになる可能性があるため、先に別の名前にしてしまう。
ridgepole でカラム名を変更するときは、 renamed_from
を使用する。
t.text "my_text_old", null: true, comment: "テキスト", renamed_from: "my_text"
t.text "my_text_tmp", null: false, comment: "テキスト"
アプリケーション側で再び元のカラムを参照するように変更し、新しいカラムを最終的な名前に変更する。
これでアプリケーションのリリースタイミングによらず、エラーにならなくなる。(更新が走ったりするのであればもっと考えることは増えると思う)
alias_attribute :my_text, :my_text_old
t.text "my_text_old", null: true, comment: "テキスト"
t.text "my_text", null: false, comment: "テキスト" , renamed_from: "my_text_tmp"
アプリケーション側で alias を外し、問題なければ古いカラムを削除する。
t.text "my_text", null: false, comment: "テキスト" , renamed_from: "my_text_tmp"
まとめ
ridgepole に限らず、 MySQL の TEXT 型は後から Not Null 付与しようと思うと色々面倒くさそうなので、初期から慎重になったほうがよい。
参考