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文
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は標準メソッドを書き換えるものもあるため、基本的なことですが、デバッグの際はプログラムだけではなく実際に行われている処理も確認するようにするように気をつけたいと思います。

一緒にスタートアップを盛り上げませんか?
enumのネガティブバージョンを定義してみた
2020/02/27
fargateを利用した環境構築の苦労
2020/04/30