[rails] 無理やり ActiveRecord の has_many に joins オプションを追加する

かなり離れたテーブルをhas_many しようと思考錯誤しました。
こんな感じの↓ で、 Group に属する User の Schedule を取ってくるという無謀な計画。

 Schedule --*..1-> User <-*..1- Member -1..*-> Group

普通にScheudle.find で取得するなら joins 使えばよかったんですが、will_paginate も使いたいのでそれだとめんどくさい。
ダメもとで、

class Group < ActiveRecord::Base
  has_many :schedules, :class_name => "Schedule", :joins => {:user => :groups}, :conditions => 'groups.id = #{id}'
end

のような風にかいてみたら、エラーになってしまいました。
has_xxx には joins ないんだね orz


なので、has_many をむりやり改造しました。


下のコードを ${RAILS_ROOT}/lib にでもいれといて、クラスのロードが終わってそうな、environment.rb の最後あたりに書いておいたら動きました。
自分が今使いたい条件で動くことしか確認してないので、別の状況で使うとエラーがもりもりでるやもしれません。


やった内容は次のとおり。
ActiveRecord::Base の create_has_many_reflection で、:joinsオプションを通るようにしてあげれば、
find するときにオプションに追加されます。
しかし、ActiveRecord::Associations::HasManyAssociation の construct_sql で生成されるSQLでは、
呼び出したモデルに、呼び出し元の外部キーがあると想定されており、条件によってはキーがないのでエラーになります。
なので、これまた無理やり通過するようにしました。
ただ、このままでは、呼び出し元のモデルと呼び出した側のモデルとの関連の条件が記述されなくなるので、
conditions オプションで指定することになります。

has_one も おなじように crete_has_one_reflection を改造すれば動くようになるのではないかと思います。

class ActiveRecord::Base
  class << self
    alias :_orig_create_has_many_reflection :create_has_many_reflection
    
    def create_has_many_reflection(association_id, options, &extension)
      class << options
        alias :_orig_assert_valid_keys :assert_valid_keys
        def assert_valid_keys(*keys)
          # joins を通すために細工
          keys << :joins
          _orig_assert_valid_keys(*keys)
        end
      end

      _orig_create_has_many_reflection(association_id, options, &extension)
    end
  end

end

class ActiveRecord::Associations::HasManyAssociation
  alias :_orig_construct_sql :construct_sql
  def construct_sql
    _orig_construct_sql
    # joins をつかった時はやりなおす(foreign_key の処理がおかしくなることがあるのではずす)
    if @reflection.options[:joins] and @reflection.options[:finder_sql].nil? and @reflection.options[:as].nil? and @reflection.options[:conter_sql].nil?
      # 使い方によって主キーの扱いがかわるので、条件はつけない
      # 条件は :conditions でつけます
      # @finder_sql = "#{@reflection.klass.table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
      # @finder_sql = "#{@owner.klass.table_name}.#{@owner.primary_key_name} = #{@owner.quoted_id}"
      @finder_sql = ""
      @finder_sql << "#{conditions}" if conditions
      @conter_sql = @finder_sql
    end
  end
end


Railsのソースはよみやすいと思うけど、さすがにつかれました。 これだけで一日が終わってしまいました。
# これまた Rails 2.0.2