ActiveRecord でテーブルをダウンタイムなしで洗い替えする方法
小ネタです。
こんな要件がありました。
「月一でこのテーブルをあるデータで洗い替えてほしい」
ActiveRecord を使うと delete_all
してから insert
すれば簡単なのですが、その間にデータへのアクセスはできずにサービスに影響を与えることになります。
調べてみると一時テーブルを用意しそのテーブルにデータを追加してから RENAME_TABLE
で元のテーブル名にリネームしてあげるとダウンタイムほぼなしで洗い替えが可能だそう。
この手順を ActiveRecord を使って行う方法を記していきます。
ActiveRecord で一時テーブル作成
これはいつものでできます。
ActiveRecord::Base.connection.create_table('temporary_table', force: true) do |t| t.string :name, comment: '名前' end
一時テーブルに対応するモデルの作成
コードベースで作成したためもちろん対応するモデルクラスがありません。
ActiveRecord::Base を継承したモデルは非常に便利で後続のデータ追加などでも利用したので、
一時的にモデルを作ります。
以下を参考にモデルを作ります。
klass = Class.new(ActiveRecord::Base) do |c| c.table_name = 'temporary_table' end Object.const_set('TemporaryTable', klass)
Rails ガイドにもありましたが、ActiveRecord::Base の table_name を書き換えるとテーブル名を上書きすることができます。
Object.const_set
は Object モジュール配下に TemporaryTable という定数を klass という値で定義するって意味らしい。
Active Record の Bulk Insert(データの一括追加)
Rails 6 だと Bulk Insert は以下のようにします。
TemporaryTable.insert_all([{name: 'aaaa'}, {name: 'bbbb'}])
データが大量の場合
データが大量の場合は MySQL のコネクションエラーが出て一度に追加できない場合があります。
each_slice を使うと一度に登録する件数を絞ることができます。
例えば 100 件ずつに絞りたい場合以下のように書きます。
data = [{name: 'aaa'}, {name: 'bbb'}, ...] data.each_slice(100) { |d| TemporaryTable.insert_all(d) }
ActiveRecord で RENAME_TABLE
今回は SQL をベタ書きして本番テーブルと作った一時テーブルをスワップさせます。
sql <<-"EOS" RENAME TABLE old_table TO t, -- 1. 古いテーブルを t というスワップ用のテーブルに退避 temporary_table TO old_table, -- 2. 新しいデータの入った temporary_table を old_table にリネーム t TO temporay_table; -- 3. 退避させていたテーブルを temporary_table にリネーム EOS # SQL 実行 ActiveRecord::Base.connection.execute(sql) # 一時テーブルを削除 ActiveRecord::Base.connection.execute("DROP TABLE temporay_table")
ActiveRecord にも rename_table というメソッドがありますが、上記のような 1 つの SQL ではできなさそうだったため、
生 SQL で実行する方法を選択しました。
この一連の流れを Rake Task なりに書き起こして Batch に設定すると定期的に洗い替えができます。