NOT NULL制約追加時のinvalid use of NULL valueを解消する
- 投稿者
- 井上
はじめまして、開発部の井上です。
本記事は既存テーブル(既存カラム)へのNOT NULL制約追加でつまずいたときのお話です。
作業前には『NULLデータに値を入れて数行のmigrationファイル作るだけ。すぐ終わるなー』と考えていたのですが、思わぬところで問題が発生しました。
はじめに作ったもの
# NOT NULL制約を追加するmigrationファイル
class ChangeColumnHoge < ActiveRecord::Migration
def change
change_column_null :hoge, :bar, false
end
end
# update_column.rb
# NULLデータを別の値へ更新するtask
desc 'barがnilの場合は1に置換する'
task hoge_bar_to_one: :environment do
Hoge.where(bar: nil).each do |hoge|
hoge.update(bar: 1)
end
end
ローカルでは問題なく上記ファイルを実行できたのですが、デプロイの際、第一の問題に遭遇しました。
デプロイ時にエラー発生
開発環境ではデータをupdateさせるtaskを実行し、その後migrationファイルを実行したため問題はありませんでしたが、CircleCIの設定では実行順序が「migration -> task」となっていたため、NOT NULLのデータが残ったままmigrationを行っており、invalid use of NULL value
といったエラーが発生していました。
対応策として、
- 先にtaskだけデプロイする
- CircleCIのconfigを編集する
などの方法がありましたが、「デプロイ用のサーバーが複数あるため一つ一つのサーバーでupdate処理を行うのが面倒」、「CircleCIのconfigをいじるようなイレギュラー作業はできるだけ避けたい」という理由から、migration時にtaskを実行する方法を取ることにしました。
migration時にrake taskを実行させる
# NOT NULL制約を追加するmigrationファイル
# rake taskを実行する処理を追加
require 'rake'
class ChangeColumnHoge < ActiveRecord::Migration
def change
Rake::Task['update_column:hoge_bar_to_one'].invoke()
change_column_null :hoge, :bar, false
end
end
これでmigration時には該当カラムのNULLデータがなくなるはずでしたが、デプロイを試して見たところ変わらずinvalid use of NULL value
のエラーが発生していました。
gemが原因?
解決できずチームに相談したところ「paranoiaで自動的に論理削除されたものを弾いてるのでは?」とアドバイスを頂きました。
paranoia
は論理削除を簡単に実装するためのgemで、paranoia
が有効になっているmodelはクエリビルダで指定しなくても、自動的に論理削除されたものを除外してくれます。
そこで、実際に発行されているSQLを確認してみました。
rails上で実行した処理
Hoge.where(bar: nil)
SELECT hoge.* FROM hoge WHERE hoge.bar IS NULL
実際に発行されていたSELECT文
SELECT hoge.* FROM hoge WHERE hoge.deleted_at IS NULL AND hoge.bar IS NULL
上記の通り、値がNULLのものを取得していたつもりでしたが、実際は論理削除済のものは除外される形となっていました。 つまり、「NULLのbarに1を格納するtask」を書いたつもりが、「論理削除されていないNULLのbarに1を格納するtask」となっていたのです。
論理削除済のデータも含めてupdateさせる
paranoia
を入れている場合、with_deleted
を使って論理削除済のデータを条件に含めることができます。
# update_column.rb
# NULLデータを別の値へ更新するtask
# 論理削除済のデータも含める
desc 'barがnilの場合は1に置換する'
task hoge_bar_to_one: :environment do
Hoge.where(bar: nil).with_deleted.each do |hoge|
hoge.update(bar: 1)
end
end
これでmigration前にtaskを実行し、論理削除済のNULLデータもupdateすることができるようになりました。
RubyGemは標準メソッドを書き換えるものもあるため、基本的なことですが、デバッグの際はプログラムだけではなく実際に行われている処理も確認するようにするように気をつけたいと思います。