ActiveRecordで同じモデルを使って別DBにデータを保存する、要するにレプリケーションなことをアプリケーション上でやりたい。単なるレプリケーションならDBだけで完結するものの、その間に何か変換するロジックをあるために、データモデルはまったく同じものを使いたい、というようなイメージ。
ETLツールいれればいいんじゃ?というのはそれはそうだけど、Railsで実装したアプリケーション上で、手っ取り早く別DBにも入れたいんだよな〜と思ったのでやってみた。
結論
# config/database.yml
production:
primary:
database: xxxxx
another:
database: yyyyy
another_model_class = Class.new(base_model_class) do
def self.name
"Another#{superclass.name.gsub(/::/, '')}"
end
end
another_model_class.establish_connection(:another)
another_model_class.insert(base_model_class.first.attributes)
解説
Active Recordには、複数のDBを切り替えて接続できる機能がある。これはあるモデルはDB1を使いたくて、別のモデルはDB2を使う、みたいなケースだとか、シャーディングするようなときにとても有効に使える。
Active Record の複数データベース対応 - Railsガイド
同じモデルを使って、DB1から読み込んでDB2に書き込む、というような挙動はこれだけでは難しい。私のアイデアでは、これを無名クラスを使って解決する。
Class.new (Ruby 3.3 リファレンスマニュアル)
このため厳密には同じモデルクラスではなく Something
と AnotherSomething
という2つのモデルクラスを使って処理する。AnotherSomething
の親となるクラスが Something
であるため、AnotherSomethingに対する操作は、Somethingと同じように振る舞う。ただし、接続先を上記の複数データベース対応を利用しAnotherSomethingは:anotherを参照するようにして、操作する先のDBはSomethingとは別のデータベースを使うことができる。
テストをどうするか
超お手軽にやるには、テスト用のDBをもう一つ用意すること。
他の方法としては、テスト実行時に自動的にDBを使うこともできる。具体的には次のようにする:
# Create another_db
ActiveRecord::Base.connection.execute("CREATE DATABASE #{another_db_name}")
# Apply DB configuration for another_db
conf = ActiveRecord::Base.configurations.configurations.find { |c| c.env_name == 'test' }
hash = {}
conf.configuration_hash.each do |k, v|
hash[k] = v
end
hash[:database] = db_name
ActiveRecord::Base.configurations.configurations << ActiveRecord::DatabaseConfigurations::UrlConfig.new(
conf.env_name,
'another',
conf.url,
hash
)
# Apply db/schema.rb for another_db
# load_schema method temporary references UrlConfig that is not reflected to ActiveRecord::Base.configurations.
ActiveRecord::Tasks::DatabaseTasks.load_schema(
ActiveRecord::DatabaseConfigurations::UrlConfig.new(
conf.env_name,
'primary',
conf.url,
hash
)
)
ActiveRecord::Base.establish_connection(:primary) # Recover primary db connection.
ちょっとハックな感じがあり、ActiveRecordの実装がいろいろ変わると動かなくなる可能性はある、、
とはいえこれで、another_dbが用意できるので、無名クラスを使ったモデルの複製+別DB接続のテストを記述していくこともできる。
subject { create(:something) }
it do
result = subject
expect(result).to eq(Something.first)
expect(result).not_to eq(AnotherSomething.first)
end
みたいな。