[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