ridgepole で MySQL の TEXT 型を後から Not Null にするとエラーになる #Rails #MySQL #ridgepole

概要

Rails で DB の構成管理に ridgepole を使っているんだけれど、 MySQL の TEXT 型に後から Not Null 制約を付けるとエラーになってしまう。

github.com

原因

例えば下記の用に定義していたとする。

# 最初は null: true で作成
t.text "my_text", null: true, comment: "テキスト"

しかし null: true にして、実行した場合には最終的に default: nil が不正になって、エラーが発生してしまう。

# Not Null にしてみる
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 値を設定できないため、エラーになる。

# 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: "テキスト"

移行先のカラムにデータ移行した後、必要であれば Railsalias_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 を外し、問題なければ古いカラムを削除する。

# alias は不要になるので削除
# 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"

まとめ

ridgepole に限らず、 MySQL の TEXT 型は後から Not Null 付与しようと思うと色々面倒くさそうなので、初期から慎重になったほうがよい。

参考