小さなエンドウ豆

まだまだいろいろ勉強中

ActiveRecord でテーブルをダウンタイムなしで洗い替えする方法

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 を継承したモデルは非常に便利で後続のデータ追加などでも利用したので、
一時的にモデルを作ります。

以下を参考にモデルを作ります。

qiita.com

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 で実行する方法を選択しました。

api.rubyonrails.org

この一連の流れを Rake Task なりに書き起こして Batch に設定すると定期的に洗い替えができます。