QA@IT

ActiveRecordのscopeに独自の状態を保存する方法

2837 PV

Slaveryという、特定のクエリーをスレーブのDBに投げるライブラリをメンテしているのですが、現状では

Slavery.on_slave { User.count }

のような記法をとっています。

しかし、これだと、最終的にクエリーを実行するタイミングでSlavery.on_slaveを実行しなければならず、多くのスコープでクエリーを組み立てる場合には

@users = User.where(...)
@users = ...
@users = Slavery.on_slave { @users.to_a }

のように最後のタイミングで明示的にto_aで強制実行してやる必要があり、あまり美しくありません。そこで、これを

@users = User.on_slave.where(...)
@users = ...

のようにして、「スレーブで実行する」という意図を、スコープそれ自体のローカルな状態として覚えておき、いつ実行されても良いようにしたいのですが、うまい方法がうかびません。(例えばクラス変数やスレッド変数に記憶するのだとまずいことになります)

どのようにすればよいでしょうか?

回答

スコープの戻り値はActiveRecord::Relationクラスのインスタンスなので、ActiveRecord::Relationのインスタンス変数に保存しておくのはどうでしょうか。

ActiveRecord::Baseを継承したクラス から アクセスする場合は where メソッドなどと同様に scoped に delegate してしまえば良いと思います。スコープを繋ぐ際は オブジェクトを clone しているので、値は引き継ぐのではないかと思います。

参考:
https://github.com/rails/rails/blob/3-2-stable/activerecord/lib/active_record/querying.rb
https://github.com/rails/rails/blob/3-2-stable/activerecord/lib/active_record/relation.rb

また、コネクションを切り替えるタイミングですが ActiveRecord::Relation#to_a を実行前に処理を追加するのが簡単そうでした。

気になる点は、ActiveRecor::exec_queries で、すでに読み込みをしている場合は @records を返しているので、コネクションの切り替えコストが高いのであれば、loaded? も利用して切り替えたほうが良いかもしれません。

編集 履歴 (0)
  • 素晴らしい!exec_queriesとcalculateをalias_method_chainするのでだいぶ行けそうです。pluckなどサポートしきれないものも残りますが、大方針が立ちました。ありがとうございます! -
ウォッチ

この質問への回答やコメントをメールでお知らせします。