[rails/rails] Arel::TreeManagerがテーブルのengineを自動的に使用可能に
背景と課題
Railsで複数のデータベースアダプターを使用する際、ArelのTreeManagerサブクラス(InsertManager、UpdateManager、DeleteManager、SelectManager)でto_sqlを呼び出すと、常にTable.engine(デフォルトではActiveRecord::Base)を使用してSQLをコンパイルしていました。
これにより、異なるデータベースアダプターを使用するモデルでは、明示的にengineを渡す必要がありました。
Arel::InsertManager.new(Model.arel_table).to_sql(Model)
この冗長な記述は、特にマルチデータベース環境での開発体験を低下させる要因となっていました。
実装内容
TreeManagerの基底クラス変更
TreeManagerクラスに新しいイニシャライザーを追加し、渡されたArel::Tableインスタンスを保持するようになりました。
def initialize(table = nil)
@table = table
end
to_sqlメソッドの改善
to_sqlメソッドが、明示的にengineが渡されない場合に、保持しているテーブルのklass(モデルクラス)を自動的に使用するようになりました。
def to_sql(engine = nil)
unless engine
table = @table.is_a?(Nodes::JoinSource) ? @table.left : @table
engine = table&.klass || Table.engine
end
collector = Arel::Collectors::SQLString.new
engine.with_connection do |connection|
connection.visitor.accept(@ast, collector).value
end
end
重要なポイントは、JoinSourceの場合は左側のテーブルを取得し、そのテーブルのklassをengineとして使用することです。klassが存在しない場合は、従来通りTable.engineにフォールバックします。
各Managerクラスの更新
DeleteManager、InsertManager、SelectManager、UpdateManagerの各クラスで、親クラスのinitializeを呼び出すように変更されました。
def initialize(table = nil)
super
@ast = Nodes::InsertStatement.new(table)
end
Tableクラスの拡張
Arel::Tableクラスにklassリーダーを追加し、テーブルに関連付けられたモデルクラスにアクセスできるようになりました。
attr_reader :table_alias, :klass
使用例
この変更により、以下のようにシンプルな記述が可能になりました。
# 変更後: engineを明示的に渡す必要がない
Arel::InsertManager.new(Model.arel_table).to_sql
# 内部的にModel.connectionが使用される
マルチデータベース環境でも、各モデルのコネクションが自動的に使用されるため、コードがより簡潔になります。
class Primary < ActiveRecord::Base
connects_to database: { writing: :primary }
end
class Secondary < ActiveRecord::Base
connects_to database: { writing: :secondary }
end
# それぞれのデータベースアダプターが自動的に使用される
Arel::InsertManager.new(Primary.arel_table).to_sql
Arel::InsertManager.new(Secondary.arel_table).to_sql
テストの追加
共通のテスト動作を定義したTreeManagerBehaviorモジュールが追加され、各Managerクラスのテストで再利用されています。このモジュールは、カスタムengineを持つテーブルでto_sqlが正しく動作することを検証します。
module TreeManagerBehavior
included do
describe "to_sql" do
it "uses given table's engine if available" do
table = Table.new(:users, klass: MyEngine)
manager = build_manager(table)
assert_includes manager.to_sql, "@users@"
end
end
end
end
まとめ
この変更により、ArelのTreeManagerクラス群がよりスマートになり、開発者はengineを明示的に渡す必要がなくなりました。特にマルチデータベース環境での開発体験が大幅に向上し、コードの可読性と保守性が改善されています。