Skip to content
This repository
Browse code

Move JoinDependency and friends from ActiveRecord::Associations::Clas…

…sMethods to just ActiveRecord::Associations
  • Loading branch information...
commit b171b9e73dcc6a89b1da652da61c5127fe605b51 1 parent d90b4e2
Jon Leighton authored February 27, 2011
4  activerecord/lib/active_record/associations.rb
@@ -5,7 +5,6 @@
5 5
 require 'active_support/core_ext/string/conversions'
6 6
 require 'active_support/core_ext/module/remove_method'
7 7
 require 'active_support/core_ext/class/attribute'
8  
-require 'active_record/associations/class_methods/join_dependency'
9 8
 
10 9
 module ActiveRecord
11 10
   class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
@@ -143,7 +142,8 @@ module Builder #:nodoc:
143 142
       autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many'
144 143
     end
145 144
 
146  
-    autoload :Preloader, 'active_record/associations/preloader'
  145
+    autoload :Preloader,      'active_record/associations/preloader'
  146
+    autoload :JoinDependency, 'active_record/associations/join_dependency'
147 147
 
148 148
     # Clears out the association cache.
149 149
     def clear_association_cache #:nodoc:
233  activerecord/lib/active_record/associations/class_methods/join_dependency.rb
... ...
@@ -1,233 +0,0 @@
1  
-require 'active_record/associations/class_methods/join_dependency/join_part'
2  
-require 'active_record/associations/class_methods/join_dependency/join_base'
3  
-require 'active_record/associations/class_methods/join_dependency/join_association'
4  
-
5  
-module ActiveRecord
6  
-  module Associations
7  
-    module ClassMethods
8  
-      class JoinDependency # :nodoc:
9  
-        attr_reader :join_parts, :reflections, :table_aliases, :active_record
10  
-
11  
-        def initialize(base, associations, joins)
12  
-          @active_record         = base
13  
-          @table_joins           = joins
14  
-          @join_parts            = [JoinBase.new(base)]
15  
-          @associations          = {}
16  
-          @reflections           = []
17  
-          @table_aliases         = Hash.new do |h,name|
18  
-            h[name] = count_aliases_from_table_joins(name.downcase)
19  
-          end
20  
-          @table_aliases[base.table_name] = 1
21  
-          build(associations)
22  
-        end
23  
-
24  
-        def graft(*associations)
25  
-          associations.each do |association|
26  
-            join_associations.detect {|a| association == a} ||
27  
-              build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
28  
-          end
29  
-          self
30  
-        end
31  
-
32  
-        def join_associations
33  
-          join_parts.last(join_parts.length - 1)
34  
-        end
35  
-
36  
-        def join_base
37  
-          join_parts.first
38  
-        end
39  
-
40  
-        def columns
41  
-          join_parts.collect { |join_part|
42  
-            table = join_part.aliased_table
43  
-            join_part.column_names_with_alias.collect{ |column_name, aliased_name|
44  
-              table[column_name].as Arel.sql(aliased_name)
45  
-            }
46  
-          }.flatten
47  
-        end
48  
-
49  
-        def count_aliases_from_table_joins(name)
50  
-          return 0 if Arel::Table === @table_joins
51  
-
52  
-          # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
53  
-          quoted_name = active_record.connection.quote_table_name(name).downcase
54  
-
55  
-          @table_joins.map { |join|
56  
-            # Table names + table aliases
57  
-            join.left.downcase.scan(
58  
-              /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
59  
-            ).size
60  
-          }.sum
61  
-        end
62  
-
63  
-        def instantiate(rows)
64  
-          primary_key = join_base.aliased_primary_key
65  
-          parents = {}
66  
-
67  
-          records = rows.map { |model|
68  
-            primary_id = model[primary_key]
69  
-            parent = parents[primary_id] ||= join_base.instantiate(model)
70  
-            construct(parent, @associations, join_associations, model)
71  
-            parent
72  
-          }.uniq
73  
-
74  
-          remove_duplicate_results!(active_record, records, @associations)
75  
-          records
76  
-        end
77  
-
78  
-        def remove_duplicate_results!(base, records, associations)
79  
-          case associations
80  
-          when Symbol, String
81  
-            reflection = base.reflections[associations]
82  
-            remove_uniq_by_reflection(reflection, records)
83  
-          when Array
84  
-            associations.each do |association|
85  
-              remove_duplicate_results!(base, records, association)
86  
-            end
87  
-          when Hash
88  
-            associations.keys.each do |name|
89  
-              reflection = base.reflections[name]
90  
-              remove_uniq_by_reflection(reflection, records)
91  
-
92  
-              parent_records = []
93  
-              records.each do |record|
94  
-                if descendant = record.send(reflection.name)
95  
-                  if reflection.collection?
96  
-                    parent_records.concat descendant.target.uniq
97  
-                  else
98  
-                    parent_records << descendant
99  
-                  end
100  
-                end
101  
-              end
102  
-
103  
-              remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
104  
-            end
105  
-          end
106  
-        end
107  
-
108  
-        protected
109  
-
110  
-        def cache_joined_association(association)
111  
-          associations = []
112  
-          parent = association.parent
113  
-          while parent != join_base
114  
-            associations.unshift(parent.reflection.name)
115  
-            parent = parent.parent
116  
-          end
117  
-          ref = @associations
118  
-          associations.each do |key|
119  
-            ref = ref[key]
120  
-          end
121  
-          ref[association.reflection.name] ||= {}
122  
-        end
123  
-
124  
-        def build(associations, parent = nil, join_type = Arel::InnerJoin)
125  
-          parent ||= join_parts.last
126  
-          case associations
127  
-          when Symbol, String
128  
-            reflection = parent.reflections[associations.to_s.intern] or
129  
-            raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
130  
-            unless join_association = find_join_association(reflection, parent)
131  
-              @reflections << reflection
132  
-              join_association = build_join_association(reflection, parent)
133  
-              join_association.join_type = join_type
134  
-              @join_parts << join_association
135  
-              cache_joined_association(join_association)
136  
-            end
137  
-            join_association
138  
-          when Array
139  
-            associations.each do |association|
140  
-              build(association, parent, join_type)
141  
-            end
142  
-          when Hash
143  
-            associations.keys.sort_by { |a| a.to_s }.each do |name|
144  
-              join_association = build(name, parent, join_type)
145  
-              build(associations[name], join_association, join_type)
146  
-            end
147  
-          else
148  
-            raise ConfigurationError, associations.inspect
149  
-          end
150  
-        end
151  
-
152  
-        def find_join_association(name_or_reflection, parent)
153  
-          if String === name_or_reflection
154  
-            name_or_reflection = name_or_reflection.to_sym
155  
-          end
156  
-
157  
-          join_associations.detect { |j|
158  
-            j.reflection == name_or_reflection && j.parent == parent
159  
-          }
160  
-        end
161  
-
162  
-        def remove_uniq_by_reflection(reflection, records)
163  
-          if reflection && reflection.collection?
164  
-            records.each { |record| record.send(reflection.name).target.uniq! }
165  
-          end
166  
-        end
167  
-
168  
-        def build_join_association(reflection, parent)
169  
-          JoinAssociation.new(reflection, self, parent)
170  
-        end
171  
-
172  
-        def construct(parent, associations, join_parts, row)
173  
-          case associations
174  
-          when Symbol, String
175  
-            name = associations.to_s
176  
-
177  
-            join_part = join_parts.detect { |j|
178  
-              j.reflection.name.to_s == name &&
179  
-                j.parent_table_name == parent.class.table_name }
180  
-
181  
-              raise(ConfigurationError, "No such association") unless join_part
182  
-
183  
-              join_parts.delete(join_part)
184  
-              construct_association(parent, join_part, row)
185  
-          when Array
186  
-            associations.each do |association|
187  
-              construct(parent, association, join_parts, row)
188  
-            end
189  
-          when Hash
190  
-            associations.sort_by { |k,_| k.to_s }.each do |association_name, assoc|
191  
-              association = construct(parent, association_name, join_parts, row)
192  
-              construct(association, assoc, join_parts, row) if association
193  
-            end
194  
-          else
195  
-            raise ConfigurationError, associations.inspect
196  
-          end
197  
-        end
198  
-
199  
-        def construct_association(record, join_part, row)
200  
-          return if record.id.to_s != join_part.parent.record_id(row).to_s
201  
-
202  
-          macro = join_part.reflection.macro
203  
-          if macro == :has_one
204  
-            return if record.association_cache.key?(join_part.reflection.name)
205  
-            association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
206  
-            set_target_and_inverse(join_part, association, record)
207  
-          else
208  
-            return if row[join_part.aliased_primary_key].nil?
209  
-            association = join_part.instantiate(row)
210  
-            case macro
211  
-            when :has_many, :has_and_belongs_to_many
212  
-              other = record.association(join_part.reflection.name)
213  
-              other.loaded!
214  
-              other.target.push(association)
215  
-              other.set_inverse_instance(association)
216  
-            when :belongs_to
217  
-              set_target_and_inverse(join_part, association, record)
218  
-            else
219  
-              raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}"
220  
-            end
221  
-          end
222  
-          association
223  
-        end
224  
-
225  
-        def set_target_and_inverse(join_part, association, record)
226  
-          other = record.association(join_part.reflection.name)
227  
-          other.target = association
228  
-          other.set_inverse_instance(association)
229  
-        end
230  
-      end
231  
-    end
232  
-  end
233  
-end
281  activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb
... ...
@@ -1,281 +0,0 @@
1  
-module ActiveRecord
2  
-  module Associations
3  
-    module ClassMethods
4  
-      class JoinDependency # :nodoc:
5  
-        class JoinAssociation < JoinPart # :nodoc:
6  
-          # The reflection of the association represented
7  
-          attr_reader :reflection
8  
-
9  
-          # The JoinDependency object which this JoinAssociation exists within. This is mainly
10  
-          # relevant for generating aliases which do not conflict with other joins which are
11  
-          # part of the query.
12  
-          attr_reader :join_dependency
13  
-
14  
-          # A JoinBase instance representing the active record we are joining onto.
15  
-          # (So in Author.has_many :posts, the Author would be that base record.)
16  
-          attr_reader :parent
17  
-
18  
-          # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
19  
-          attr_accessor :join_type
20  
-
21  
-          # These implement abstract methods from the superclass
22  
-          attr_reader :aliased_prefix, :aliased_table_name
23  
-
24  
-          delegate :options, :through_reflection, :source_reflection, :to => :reflection
25  
-          delegate :table, :table_name, :to => :parent, :prefix => :parent
26  
-
27  
-          def initialize(reflection, join_dependency, parent = nil)
28  
-            reflection.check_validity!
29  
-
30  
-            if reflection.options[:polymorphic]
31  
-              raise EagerLoadPolymorphicError.new(reflection)
32  
-            end
33  
-
34  
-            super(reflection.klass)
35  
-
36  
-            @reflection      = reflection
37  
-            @join_dependency = join_dependency
38  
-            @parent          = parent
39  
-            @join_type       = Arel::InnerJoin
40  
-            @aliased_prefix  = "t#{ join_dependency.join_parts.size }"
41  
-
42  
-            # This must be done eagerly upon initialisation because the alias which is produced
43  
-            # depends on the state of the join dependency, but we want it to work the same way
44  
-            # every time.
45  
-            allocate_aliases
46  
-            @table = Arel::Table.new(
47  
-              table_name, :as => aliased_table_name, :engine => arel_engine
48  
-            )
49  
-          end
50  
-
51  
-          def ==(other)
52  
-            other.class == self.class &&
53  
-              other.reflection == reflection &&
54  
-              other.parent == parent
55  
-          end
56  
-
57  
-          def find_parent_in(other_join_dependency)
58  
-            other_join_dependency.join_parts.detect do |join_part|
59  
-              parent == join_part
60  
-            end
61  
-          end
62  
-
63  
-          def join_to(relation)
64  
-            send("join_#{reflection.macro}_to", relation)
65  
-          end
66  
-
67  
-          def join_relation(joining_relation)
68  
-            self.join_type = Arel::OuterJoin
69  
-            joining_relation.joins(self)
70  
-          end
71  
-
72  
-          attr_reader :table
73  
-          # More semantic name given we are talking about associations
74  
-          alias_method :target_table, :table
75  
-
76  
-          protected
77  
-
78  
-          def aliased_table_name_for(name, suffix = nil)
79  
-            aliases = @join_dependency.table_aliases
80  
-
81  
-            if aliases[name] != 0 # We need an alias
82  
-              connection = active_record.connection
83  
-
84  
-              name = connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
85  
-              aliases[name] += 1
86  
-              name = name[0, connection.table_alias_length-3] + "_#{aliases[name]}" if aliases[name] > 1
87  
-            else
88  
-              aliases[name] += 1
89  
-            end
90  
-
91  
-            name
92  
-          end
93  
-
94  
-          def pluralize(table_name)
95  
-            ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
96  
-          end
97  
-
98  
-          private
99  
-
100  
-          def allocate_aliases
101  
-            @aliased_table_name = aliased_table_name_for(table_name)
102  
-
103  
-            if reflection.macro == :has_and_belongs_to_many
104  
-              @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
105  
-            elsif [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through]
106  
-              @aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
107  
-            end
108  
-          end
109  
-
110  
-          def process_conditions(conditions, table_name)
111  
-            if conditions.respond_to?(:to_proc)
112  
-              conditions = instance_eval(&conditions)
113  
-            end
114  
-
115  
-            Arel.sql(sanitize_sql(conditions, table_name))
116  
-          end
117  
-
118  
-          def sanitize_sql(condition, table_name)
119  
-            active_record.send(:sanitize_sql, condition, table_name)
120  
-          end
121  
-
122  
-          def join_target_table(relation, condition)
123  
-            conditions = [condition]
124  
-
125  
-            # If the target table is an STI model then we must be sure to only include records of
126  
-            # its type and its sub-types.
127  
-            unless active_record.descends_from_active_record?
128  
-              sti_column    = target_table[active_record.inheritance_column]
129  
-              subclasses    = active_record.descendants
130  
-              sti_condition = sti_column.eq(active_record.sti_name)
131  
-
132  
-              conditions << subclasses.inject(sti_condition) { |attr,subclass|
133  
-                attr.or(sti_column.eq(subclass.sti_name))
134  
-              }
135  
-            end
136  
-
137  
-            # If the reflection has conditions, add them
138  
-            if options[:conditions]
139  
-              conditions << process_conditions(options[:conditions], aliased_table_name)
140  
-            end
141  
-
142  
-            ands = relation.create_and(conditions)
143  
-
144  
-            join = relation.create_join(
145  
-              target_table,
146  
-              relation.create_on(ands),
147  
-              join_type)
148  
-
149  
-            relation.from join
150  
-          end
151  
-
152  
-          def join_has_and_belongs_to_many_to(relation)
153  
-            join_table = Arel::Table.new(
154  
-              options[:join_table]
155  
-            ).alias(@aliased_join_table_name)
156  
-
157  
-            fk       = options[:foreign_key]             || reflection.active_record.to_s.foreign_key
158  
-            klass_fk = options[:association_foreign_key] || reflection.klass.to_s.foreign_key
159  
-
160  
-            relation = relation.join(join_table, join_type)
161  
-            relation = relation.on(
162  
-              join_table[fk].
163  
-              eq(parent_table[reflection.active_record.primary_key])
164  
-            )
165  
-
166  
-            join_target_table(
167  
-              relation,
168  
-              target_table[reflection.klass.primary_key].
169  
-              eq(join_table[klass_fk])
170  
-            )
171  
-          end
172  
-
173  
-          def join_has_many_to(relation)
174  
-            if reflection.options[:through]
175  
-              join_has_many_through_to(relation)
176  
-            elsif reflection.options[:as]
177  
-              join_has_many_polymorphic_to(relation)
178  
-            else
179  
-              foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
180  
-              primary_key = options[:primary_key] || parent.primary_key
181  
-
182  
-              join_target_table(
183  
-                relation,
184  
-                target_table[foreign_key].
185  
-                eq(parent_table[primary_key])
186  
-              )
187  
-            end
188  
-          end
189  
-          alias :join_has_one_to :join_has_many_to
190  
-
191  
-          def join_has_many_through_to(relation)
192  
-            join_table = Arel::Table.new(
193  
-              through_reflection.klass.table_name
194  
-            ).alias @aliased_join_table_name
195  
-
196  
-            jt_conditions = []
197  
-            first_key = second_key = nil
198  
-
199  
-            if through_reflection.macro == :belongs_to
200  
-              jt_primary_key = through_reflection.foreign_key
201  
-              jt_foreign_key = through_reflection.association_primary_key
202  
-            else
203  
-              jt_primary_key = through_reflection.active_record_primary_key
204  
-              jt_foreign_key = through_reflection.foreign_key
205  
-
206  
-              if through_reflection.options[:as] # has_many :through against a polymorphic join
207  
-                jt_conditions <<
208  
-                join_table["#{through_reflection.options[:as]}_type"].
209  
-                  eq(parent.active_record.base_class.name)
210  
-              end
211  
-            end
212  
-
213  
-            case source_reflection.macro
214  
-            when :has_many
215  
-              second_key = options[:foreign_key] || primary_key
216  
-
217  
-              if source_reflection.options[:as]
218  
-                first_key = "#{source_reflection.options[:as]}_id"
219  
-              else
220  
-                first_key = through_reflection.klass.base_class.to_s.foreign_key
221  
-              end
222  
-
223  
-              unless through_reflection.klass.descends_from_active_record?
224  
-                jt_conditions <<
225  
-                join_table[through_reflection.active_record.inheritance_column].
226  
-                  eq(through_reflection.klass.sti_name)
227  
-              end
228  
-            when :belongs_to
229  
-              first_key = primary_key
230  
-
231  
-              if reflection.options[:source_type]
232  
-                second_key = source_reflection.association_foreign_key
233  
-
234  
-                jt_conditions <<
235  
-                join_table[reflection.source_reflection.foreign_type].
236  
-                  eq(reflection.options[:source_type])
237  
-              else
238  
-                second_key = source_reflection.foreign_key
239  
-              end
240  
-            end
241  
-
242  
-            jt_conditions <<
243  
-            parent_table[jt_primary_key].
244  
-              eq(join_table[jt_foreign_key])
245  
-
246  
-            if through_reflection.options[:conditions]
247  
-              jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_name)
248  
-            end
249  
-
250  
-            relation = relation.join(join_table, join_type).on(*jt_conditions)
251  
-
252  
-            join_target_table(
253  
-              relation,
254  
-              target_table[first_key].eq(join_table[second_key])
255  
-            )
256  
-          end
257  
-
258  
-          def join_has_many_polymorphic_to(relation)
259  
-            join_target_table(
260  
-              relation,
261  
-              target_table["#{reflection.options[:as]}_id"].
262  
-              eq(parent_table[parent.primary_key]).and(
263  
-              target_table["#{reflection.options[:as]}_type"].
264  
-              eq(parent.active_record.base_class.name))
265  
-            )
266  
-          end
267  
-
268  
-          def join_belongs_to_to(relation)
269  
-            foreign_key = options[:foreign_key] || reflection.foreign_key
270  
-            primary_key = options[:primary_key] || reflection.klass.primary_key
271  
-
272  
-            join_target_table(
273  
-              relation,
274  
-              target_table[primary_key].eq(parent_table[foreign_key])
275  
-            )
276  
-          end
277  
-        end
278  
-      end
279  
-    end
280  
-  end
281  
-end
26  activerecord/lib/active_record/associations/class_methods/join_dependency/join_base.rb
... ...
@@ -1,26 +0,0 @@
1  
-module ActiveRecord
2  
-  module Associations
3  
-    module ClassMethods
4  
-      class JoinDependency # :nodoc:
5  
-        class JoinBase < JoinPart # :nodoc:
6  
-          def ==(other)
7  
-            other.class == self.class &&
8  
-              other.active_record == active_record
9  
-          end
10  
-
11  
-          def aliased_prefix
12  
-            "t0"
13  
-          end
14  
-
15  
-          def table
16  
-            Arel::Table.new(table_name, arel_engine)
17  
-          end
18  
-
19  
-          def aliased_table_name
20  
-            active_record.table_name
21  
-          end
22  
-        end
23  
-      end
24  
-    end
25  
-  end
26  
-end
80  activerecord/lib/active_record/associations/class_methods/join_dependency/join_part.rb
... ...
@@ -1,80 +0,0 @@
1  
-module ActiveRecord
2  
-  module Associations
3  
-    module ClassMethods
4  
-      class JoinDependency # :nodoc:
5  
-        # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited
6  
-        # by JoinBase and JoinAssociation. A JoinBase represents the Active Record which
7  
-        # everything else is being joined onto. A JoinAssociation represents an association which
8  
-        # is joining to the base. A JoinAssociation may result in more than one actual join
9  
-        # operations (for example a has_and_belongs_to_many JoinAssociation would result in
10  
-        # two; one for the join table and one for the target table).
11  
-        class JoinPart # :nodoc:
12  
-          # The Active Record class which this join part is associated 'about'; for a JoinBase
13  
-          # this is the actual base model, for a JoinAssociation this is the target model of the
14  
-          # association.
15  
-          attr_reader :active_record
16  
-
17  
-          delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :active_record
18  
-
19  
-          def initialize(active_record)
20  
-            @active_record = active_record
21  
-            @cached_record = {}
22  
-            @column_names_with_alias = nil
23  
-          end
24  
-
25  
-          def aliased_table
26  
-            Arel::Nodes::TableAlias.new aliased_table_name, table
27  
-          end
28  
-
29  
-          def ==(other)
30  
-            raise NotImplementedError
31  
-          end
32  
-
33  
-          # An Arel::Table for the active_record
34  
-          def table
35  
-            raise NotImplementedError
36  
-          end
37  
-
38  
-          # The prefix to be used when aliasing columns in the active_record's table
39  
-          def aliased_prefix
40  
-            raise NotImplementedError
41  
-          end
42  
-
43  
-          # The alias for the active_record's table
44  
-          def aliased_table_name
45  
-            raise NotImplementedError
46  
-          end
47  
-
48  
-          # The alias for the primary key of the active_record's table
49  
-          def aliased_primary_key
50  
-            "#{aliased_prefix}_r0"
51  
-          end
52  
-
53  
-          # An array of [column_name, alias] pairs for the table
54  
-          def column_names_with_alias
55  
-            unless @column_names_with_alias
56  
-              @column_names_with_alias = []
57  
-
58  
-              ([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i|
59  
-                @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
60  
-              end
61  
-            end
62  
-            @column_names_with_alias
63  
-          end
64  
-
65  
-          def extract_record(row)
66  
-            Hash[column_names_with_alias.map{|cn, an| [cn, row[an]]}]
67  
-          end
68  
-
69  
-          def record_id(row)
70  
-            row[aliased_primary_key]
71  
-          end
72  
-
73  
-          def instantiate(row)
74  
-            @cached_record[record_id(row)] ||= active_record.send(:instantiate, extract_record(row))
75  
-          end
76  
-        end
77  
-      end
78  
-    end
79  
-  end
80  
-end
231  activerecord/lib/active_record/associations/join_dependency.rb
... ...
@@ -0,0 +1,231 @@
  1
+module ActiveRecord
  2
+  module Associations
  3
+    class JoinDependency # :nodoc:
  4
+      autoload :JoinPart,        'active_record/associations/join_dependency/join_part'
  5
+      autoload :JoinBase,        'active_record/associations/join_dependency/join_base'
  6
+      autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association'
  7
+
  8
+      attr_reader :join_parts, :reflections, :table_aliases, :active_record
  9
+
  10
+      def initialize(base, associations, joins)
  11
+        @active_record         = base
  12
+        @table_joins           = joins
  13
+        @join_parts            = [JoinBase.new(base)]
  14
+        @associations          = {}
  15
+        @reflections           = []
  16
+        @table_aliases         = Hash.new do |h,name|
  17
+          h[name] = count_aliases_from_table_joins(name.downcase)
  18
+        end
  19
+        @table_aliases[base.table_name] = 1
  20
+        build(associations)
  21
+      end
  22
+
  23
+      def graft(*associations)
  24
+        associations.each do |association|
  25
+          join_associations.detect {|a| association == a} ||
  26
+            build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
  27
+        end
  28
+        self
  29
+      end
  30
+
  31
+      def join_associations
  32
+        join_parts.last(join_parts.length - 1)
  33
+      end
  34
+
  35
+      def join_base
  36
+        join_parts.first
  37
+      end
  38
+
  39
+      def columns
  40
+        join_parts.collect { |join_part|
  41
+          table = join_part.aliased_table
  42
+          join_part.column_names_with_alias.collect{ |column_name, aliased_name|
  43
+            table[column_name].as Arel.sql(aliased_name)
  44
+          }
  45
+        }.flatten
  46
+      end
  47
+
  48
+      def count_aliases_from_table_joins(name)
  49
+        return 0 if Arel::Table === @table_joins
  50
+
  51
+        # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
  52
+        quoted_name = active_record.connection.quote_table_name(name).downcase
  53
+
  54
+        @table_joins.map { |join|
  55
+          # Table names + table aliases
  56
+          join.left.downcase.scan(
  57
+            /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
  58
+          ).size
  59
+        }.sum
  60
+      end
  61
+
  62
+      def instantiate(rows)
  63
+        primary_key = join_base.aliased_primary_key
  64
+        parents = {}
  65
+
  66
+        records = rows.map { |model|
  67
+          primary_id = model[primary_key]
  68
+          parent = parents[primary_id] ||= join_base.instantiate(model)
  69
+          construct(parent, @associations, join_associations, model)
  70
+          parent
  71
+        }.uniq
  72
+
  73
+        remove_duplicate_results!(active_record, records, @associations)
  74
+        records
  75
+      end
  76
+
  77
+      def remove_duplicate_results!(base, records, associations)
  78
+        case associations
  79
+        when Symbol, String
  80
+          reflection = base.reflections[associations]
  81
+          remove_uniq_by_reflection(reflection, records)
  82
+        when Array
  83
+          associations.each do |association|
  84
+            remove_duplicate_results!(base, records, association)
  85
+          end
  86
+        when Hash
  87
+          associations.keys.each do |name|
  88
+            reflection = base.reflections[name]
  89
+            remove_uniq_by_reflection(reflection, records)
  90
+
  91
+            parent_records = []
  92
+            records.each do |record|
  93
+              if descendant = record.send(reflection.name)
  94
+                if reflection.collection?
  95
+                  parent_records.concat descendant.target.uniq
  96
+                else
  97
+                  parent_records << descendant
  98
+                end
  99
+              end
  100
+            end
  101
+
  102
+            remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
  103
+          end
  104
+        end
  105
+      end
  106
+
  107
+      protected
  108
+
  109
+      def cache_joined_association(association)
  110
+        associations = []
  111
+        parent = association.parent
  112
+        while parent != join_base
  113
+          associations.unshift(parent.reflection.name)
  114
+          parent = parent.parent
  115
+        end
  116
+        ref = @associations
  117
+        associations.each do |key|
  118
+          ref = ref[key]
  119
+        end
  120
+        ref[association.reflection.name] ||= {}
  121
+      end
  122
+
  123
+      def build(associations, parent = nil, join_type = Arel::InnerJoin)
  124
+        parent ||= join_parts.last
  125
+        case associations
  126
+        when Symbol, String
  127
+          reflection = parent.reflections[associations.to_s.intern] or
  128
+          raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
  129
+          unless join_association = find_join_association(reflection, parent)
  130
+            @reflections << reflection
  131
+            join_association = build_join_association(reflection, parent)
  132
+            join_association.join_type = join_type
  133
+            @join_parts << join_association
  134
+            cache_joined_association(join_association)
  135
+          end
  136
+          join_association
  137
+        when Array
  138
+          associations.each do |association|
  139
+            build(association, parent, join_type)
  140
+          end
  141
+        when Hash
  142
+          associations.keys.sort_by { |a| a.to_s }.each do |name|
  143
+            join_association = build(name, parent, join_type)
  144
+            build(associations[name], join_association, join_type)
  145
+          end
  146
+        else
  147
+          raise ConfigurationError, associations.inspect
  148
+        end
  149
+      end
  150
+
  151
+      def find_join_association(name_or_reflection, parent)
  152
+        if String === name_or_reflection
  153
+          name_or_reflection = name_or_reflection.to_sym
  154
+        end
  155
+
  156
+        join_associations.detect { |j|
  157
+          j.reflection == name_or_reflection && j.parent == parent
  158
+        }
  159
+      end
  160
+
  161
+      def remove_uniq_by_reflection(reflection, records)
  162
+        if reflection && reflection.collection?
  163
+          records.each { |record| record.send(reflection.name).target.uniq! }
  164
+        end
  165
+      end
  166
+
  167
+      def build_join_association(reflection, parent)
  168
+        JoinAssociation.new(reflection, self, parent)
  169
+      end
  170
+
  171
+      def construct(parent, associations, join_parts, row)
  172
+        case associations
  173
+        when Symbol, String
  174
+          name = associations.to_s
  175
+
  176
+          join_part = join_parts.detect { |j|
  177
+            j.reflection.name.to_s == name &&
  178
+              j.parent_table_name == parent.class.table_name }
  179
+
  180
+            raise(ConfigurationError, "No such association") unless join_part
  181
+
  182
+            join_parts.delete(join_part)
  183
+            construct_association(parent, join_part, row)
  184
+        when Array
  185
+          associations.each do |association|
  186
+            construct(parent, association, join_parts, row)
  187
+          end
  188
+        when Hash
  189
+          associations.sort_by { |k,_| k.to_s }.each do |association_name, assoc|
  190
+            association = construct(parent, association_name, join_parts, row)
  191
+            construct(association, assoc, join_parts, row) if association
  192
+          end
  193
+        else
  194
+          raise ConfigurationError, associations.inspect
  195
+        end
  196
+      end
  197
+
  198
+      def construct_association(record, join_part, row)
  199
+        return if record.id.to_s != join_part.parent.record_id(row).to_s
  200
+
  201
+        macro = join_part.reflection.macro
  202
+        if macro == :has_one
  203
+          return if record.association_cache.key?(join_part.reflection.name)
  204
+          association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
  205
+          set_target_and_inverse(join_part, association, record)
  206
+        else
  207
+          return if row[join_part.aliased_primary_key].nil?
  208
+          association = join_part.instantiate(row)
  209
+          case macro
  210
+          when :has_many, :has_and_belongs_to_many
  211
+            other = record.association(join_part.reflection.name)
  212
+            other.loaded!
  213
+            other.target.push(association)
  214
+            other.set_inverse_instance(association)
  215
+          when :belongs_to
  216
+            set_target_and_inverse(join_part, association, record)
  217
+          else
  218
+            raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}"
  219
+          end
  220
+        end
  221
+        association
  222
+      end
  223
+
  224
+      def set_target_and_inverse(join_part, association, record)
  225
+        other = record.association(join_part.reflection.name)
  226
+        other.target = association
  227
+        other.set_inverse_instance(association)
  228
+      end
  229
+    end
  230
+  end
  231
+end
279  activerecord/lib/active_record/associations/join_dependency/join_association.rb
... ...
@@ -0,0 +1,279 @@
  1
+module ActiveRecord
  2
+  module Associations
  3
+    class JoinDependency # :nodoc:
  4
+      class JoinAssociation < JoinPart # :nodoc:
  5
+        # The reflection of the association represented
  6
+        attr_reader :reflection
  7
+
  8
+        # The JoinDependency object which this JoinAssociation exists within. This is mainly
  9
+        # relevant for generating aliases which do not conflict with other joins which are
  10
+        # part of the query.
  11
+        attr_reader :join_dependency
  12
+
  13
+        # A JoinBase instance representing the active record we are joining onto.
  14
+        # (So in Author.has_many :posts, the Author would be that base record.)
  15
+        attr_reader :parent
  16
+
  17
+        # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
  18
+        attr_accessor :join_type
  19
+
  20
+        # These implement abstract methods from the superclass
  21
+        attr_reader :aliased_prefix, :aliased_table_name
  22
+
  23
+        delegate :options, :through_reflection, :source_reflection, :to => :reflection
  24
+        delegate :table, :table_name, :to => :parent, :prefix => :parent
  25
+
  26
+        def initialize(reflection, join_dependency, parent = nil)
  27
+          reflection.check_validity!
  28
+
  29
+          if reflection.options[:polymorphic]
  30
+            raise EagerLoadPolymorphicError.new(reflection)
  31
+          end
  32
+
  33
+          super(reflection.klass)
  34
+
  35
+          @reflection      = reflection
  36
+          @join_dependency = join_dependency
  37
+          @parent          = parent
  38
+          @join_type       = Arel::InnerJoin
  39
+          @aliased_prefix  = "t#{ join_dependency.join_parts.size }"
  40
+
  41
+          # This must be done eagerly upon initialisation because the alias which is produced
  42
+          # depends on the state of the join dependency, but we want it to work the same way
  43
+          # every time.
  44
+          allocate_aliases
  45
+          @table = Arel::Table.new(
  46
+            table_name, :as => aliased_table_name, :engine => arel_engine
  47
+          )
  48
+        end
  49
+
  50
+        def ==(other)
  51
+          other.class == self.class &&
  52
+            other.reflection == reflection &&
  53
+            other.parent == parent
  54
+        end
  55
+
  56
+        def find_parent_in(other_join_dependency)
  57
+          other_join_dependency.join_parts.detect do |join_part|
  58
+            parent == join_part
  59
+          end
  60
+        end
  61
+
  62
+        def join_to(relation)
  63
+          send("join_#{reflection.macro}_to", relation)
  64
+        end
  65
+
  66
+        def join_relation(joining_relation)
  67
+          self.join_type = Arel::OuterJoin
  68
+          joining_relation.joins(self)
  69
+        end
  70
+
  71
+        attr_reader :table
  72
+        # More semantic name given we are talking about associations
  73
+        alias_method :target_table, :table
  74
+
  75
+        protected
  76
+
  77
+        def aliased_table_name_for(name, suffix = nil)
  78
+          aliases = @join_dependency.table_aliases
  79
+
  80
+          if aliases[name] != 0 # We need an alias
  81
+            connection = active_record.connection
  82
+
  83
+            name = connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
  84
+            aliases[name] += 1
  85
+            name = name[0, connection.table_alias_length-3] + "_#{aliases[name]}" if aliases[name] > 1
  86
+          else
  87
+            aliases[name] += 1
  88
+          end
  89
+
  90
+          name
  91
+        end
  92
+
  93
+        def pluralize(table_name)
  94
+          ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
  95
+        end
  96
+
  97
+        private
  98
+
  99
+        def allocate_aliases
  100
+          @aliased_table_name = aliased_table_name_for(table_name)
  101
+
  102
+          if reflection.macro == :has_and_belongs_to_many
  103
+            @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
  104
+          elsif [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through]
  105
+            @aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
  106
+          end
  107
+        end
  108
+
  109
+        def process_conditions(conditions, table_name)
  110
+          if conditions.respond_to?(:to_proc)
  111
+            conditions = instance_eval(&conditions)
  112
+          end
  113
+