ActiveRecord_Associations_CollectionProxy ってなんだよ!

rails でだいぶ無茶しなきゃいけないことがあったので備忘録

* * *

 ECサイトのカスタマイズで見積書を出す機能を追加したのですが、合計金額算出時に、ActiveRecord の sum メソッドを使用していたため見積りの内容を「注文として」一度DBに保存しないといけないという事実が発覚。
(※ sum メソッドは、”SELECT count(メソッド引数) FROM テーブル名” というクエリを発行するため、保存していないと 0 が返ってくる)
 注文として保存してしまうとログに残ったり、見積書作成中に下手にページ遷移されると危険なので sum メソッドの挙動を変更することにしたのですが、sum メソッドはいろいろなところで使ってるためモンキーパッチを当てるわけにも行かない。
 かと言って同じメンバを持つダミークラスを作ろうとすると、今度はせっかくフレームワークに実装されている消費税計算・経費計算が使えなくなってしまう。
 それらまで移植するのはゼッタイに嫌だったので、見積り算出時のみ sum メソッドでクエリを発行しないようにするコードをガリガリと書いてみました。

 それがこちら

class DummyOrder < Order
  self.table_name = 'orders'

  def initialize
    super nil, {}

    # LineItem モデルのリレーションを上書きする
    association_instance_set :line_items,
                             DummyLineItem::DummyHasManyAssociation.new(self, self.class.reflections[:line_items])
  end
end

# 見積書作成時に使うダミーLineItem
class DummyLineItem < LineItem
  self.table_name = 'line_item'

  # LineItem の CollectionProxy を騙す
  class DummyHasManyAssociation < ActiveRecord::Associations::HasManyAssociation
    def reader(force_reload = false)
      if force_reload
        klass.uncached { reload }
      elsif stale_target?
        reload
      end

      @proxy ||= DummyCollectionProxy.new DummyLineItem, self
    end
  end

  # sum メソッドを DB の呼び出しから、自身の持つ配列から算出するよう修正
  class DummyCollectionProxy < LineItem::ActiveRecord_Associations_CollectionProxy
    def sum(args)
      case args.to_s
      when "price * quantity"
        reduce(0) { |a, e| a + e.price * e.quantity }
      else
        reduce(0) { |a, e| a + e.send(args.to_s).to_i }
      end
    end
  end
end

…カオス。

一応解説を書いておくとですね、
 OrderモデルとLineItemモデルが1対多関係にあり、Orderモデルが line_items メソッドを持っているのですが、initialize で association_instance_setを呼び出し、line_items メソッドで返される LineItem::ActiveRecord_Associations_CollectionProxy インスタンスを DummyLineItem::DummyHasManyAssociation インスタンスに置き換えます。
 ダミークラス「DummyHasManyAssociation」のreaderは、元の「ActiveRecord::Associations::HasManyAssociation」クラスのreaderをほぼ丸コピーなのですが、最後の1行は書き換えてます。

@proxy ||= DummyCollectionProxy.new DummyLineItem, self

コレのことです。

ちなみに、元々のコードは下記のとおりです。
(activerecord-4.1.6/lib/active_record/associations/collection_association.rb)

@proxy ||= CollectionProxy.create(klass, self)

 ざっくり追った感じ、Reflection オブジェクト(has_manyとか指定すると自動的に作成されるリレーション用のオブジェクト)から、対象のテーブルに紐付いたモデルオブジェクトのCollectionProxyインスタンスを作成して返す、という挙動をしているみたいですが、今回は特定条件下でしか使わない前提だったので今回作ったDummyCollectionProxyインスタンスを返すようにしてます。
 これをやらないとActiveRecord が自動生成したCollectionProxyが生成されてしまいます。
(self.class.reflections で呼び出している Reflection オブジェクトの中身を書き換えられればいいのですが、コレは実態のあるテーブルと密に紐付いており、変更が面倒臭かった今回のケースではそこまで手を入れる必要がなかったのでHasManyAssociationを置き換える方法を取りました)

 最後に、DummyCollectionProxy の sum メソッドをオーバーライドしてやればOKです。

 見積り作成時にはこのDummyOrder を生成してやればDBを使うことなく合計値が出せるようになりました。

…もっとスマートな方法があったら誰か教えてください。

RSS / feedly
  • follow us in feedly
  • follow us in feedly
ソーシャル
広告