2006-05-14 優しいRailsの育て方 [長年日記]

[Rails] 関連名による INNER JOIN が欲しい

从 ’w’)<舞波ね、悩みがあるの

groups
id
name
has_many
members
id
group_id
name
has_many
favorites
id
member_id
name

从*’w’)<OUTER JOIN はすっごいエレガントなの♪

Member.find(:all, :include=>[:group, :favorites])

从 ;゜w゜)<でも、INNER JOIN だと生SQLに逆戻り!!

Member.find(:all, :joins=>"INNER JOIN groups ON groups.id = members.group_id INNER JOIN favorites ...")
       _, ,_ 
     从 ’w’) ∩ < ヤダヤダ、もう耐えられないのー!シンボル名でJOINしたいの〜!
     ⊂   ( 
       ヽ∩ つ  ジタバタ 
         〃〃

symbolic join

vendor/plugins/symbolic_join/init.rb

class ActiveRecord::Base
  class << self
    private
    def add_joins!(sql, options, scope = :auto)
      scope = scope(:find) if :auto == scope
      join = (scope && scope[:joins]) || options[:joins]
      sql << " #{expand_join_query(join)} " if join
    end

    def expand_join_query(*joins)
      joins.flatten.map{|join|
        case join
        when Symbol
          ref = reflections[join] or
            raise ActiveRecord::ActiveRecordError, "Could not find the source association :#{join} in model #{self}"
          case ref.macro
          when :belongs_to
            "INNER JOIN %s ON %s.%s = %s.%s" % [ref.table_name, ref.table_name, primary_key, table_name, ref.primary_key_name]
          else
            "INNER JOIN %s ON %s.%s = %s.%s" % [ref.table_name, ref.table_name, ref.primary_key_name, table_name, primary_key]
          end
        else
          join.to_s
        end
      }.join(" ")
    end
  end
end

从*’w’)<とりあえずこれで以下は動くようになる

Member.find(:all, :joins=>[:group, :favorites])
  ○☆ノ_,,_ ○ 
 ((从# ’w’) ))<でも、Cascade マニアとしては多段できないと嫌なの!意地でも対応させるの!
 ○ (   ) ○

从 ;゜w゜)<多段対応したら、ちっちゃくなっちゃった!

class ActiveRecord::Base
  class << self
    private
    def add_joins!(sql, options, scope = :auto)
      scope = scope(:find) if :auto == scope
      join = (scope && scope[:joins]) || options[:joins]
      sql << " #{expand_join_query(join)} " if join
    end

    def expand_join_query(*joins)
      strings, joins = joins.flatten.partition{|i| i.class == String}
      string_join = strings.join(' ')
      left_joins = JoinDependency.new(self, joins, string_join).join_associations.map(&:association_join)
      left_joins.join(" ").gsub(/LEFT OUTER/, "INNER") + string_join
    end
  end
end

JoinDependency クラスを無駄に作ってた舞波は、反YAGNI界の神。

从*’w’)<クゥ〜ン♪

Member.find(:all, :joins=>[:group, :favorites])
=> SELECT * FROM members
   INNER JOIN groups    ON groups.id = members.group_id
   INNER JOIN favorites ON favorites.member_id = members.id 

Member.find(:all, :joins=>[:group, "INNER JOIN prefs USING(prefcode)"])
=> SELECT * FROM members
   INNER JOIN groups    ON groups.id = members.group_id
   INNER JOIN prefs     USING(prefcode) 

Group.find(:all, :joins=>[{:members=>:favorites}, :songs])
=> SELECT * FROM groups
   INNER JOIN members   ON members.group_id = groups.id
   INNER JOIN favorites ON favorites.member_id = members.id
   INNER JOIN songs     ON songs.group_id = groups.id 

一応プラグイン (上記のままだけれどディレクトリを掘るのも面倒な人向け)

ruby script/plugin install http://wota.jp/svn/rails/plugins/branches/stable/symbolic_join

特徴

  • 関連名をシンボル名で指定したINNER JOIN
  • 従来の文字列(生SQL)との併用
  • Cascaded 対応

制限

  • INNER JOIN 強制 (LEFT は :include を使う方向で)
  • 1:多をJOINすると結果セット数が増える (一覧表示用で)
  • :include と併用は不明 (何が起こるのか不明)

本気で考えていないのでどこまで使えるかは不明です。(特に Association 系で)。あと、LEFT OUTER を gsub で置換してる手抜き処理なのでいつまで使えるかも無保証です。

今後

正しくは JoinAssociation#association_join(join_type = "LEFT OUTER") にすベッキー。で、JOIN 節の生成は各 Reflection に dispatch した方がいい気がする。Reflection は自分の「関係」をもとに結合構造を作り、具体的な表記に関してはさらに ConnectionAdapters に dispatch して、各DBのシンタックスの差異を吸収する・・・とかどーたらこーたら、3点シュートみたいな恋がしたい現実。じゃないともう association_join は手がつけられましぇん。うまく行けば RubyKaigi のネタになるかな?


サイト内検索 (by Google)

| JRuby | Rails | Berryz | ℃-ute | エッグ | jQuery |

過去

2006年
5月
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31

未来

コンタクト