From b0498372a10bda006350af42708a5588ab28ffcb Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 26 Dec 2010 19:37:35 +0000 Subject: Add a HasAssociation module for common code for has_* associations --- activerecord/lib/active_record/associations.rb | 2 + .../associations/association_collection.rb | 2 + .../associations/association_proxy.rb | 59 +------ .../active_record/associations/has_association.rb | 54 +++++++ .../associations/has_many_through_association.rb | 3 +- .../associations/has_one_association.rb | 2 + .../associations/has_one_through_association.rb | 4 +- .../associations/through_association.rb | 169 +++++++++++++++++++++ .../associations/through_association_scope.rb | 169 --------------------- 9 files changed, 236 insertions(+), 228 deletions(-) create mode 100644 activerecord/lib/active_record/associations/has_association.rb create mode 100644 activerecord/lib/active_record/associations/through_association.rb delete mode 100644 activerecord/lib/active_record/associations/through_association_scope.rb (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 1056c51a3d..b49cf8de95 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -120,6 +120,8 @@ module ActiveRecord # So there is no need to eager load them. autoload :AssociationCollection, 'active_record/associations/association_collection' autoload :AssociationProxy, 'active_record/associations/association_proxy' + autoload :HasAssociation, 'active_record/associations/has_association' + autoload :ThroughAssociation, 'active_record/associations/through_association' autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association' autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association' autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association' diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index c9f9c925b0..86cc17394c 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -18,6 +18,8 @@ module ActiveRecord # If you need to work on all current children, new and existing records, # +load_target+ and the +loaded+ flag are your friends. class AssociationCollection < AssociationProxy #:nodoc: + include HasAssociation + delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped def select(select = nil) diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index c7b171b41d..b60aa33c98 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -4,17 +4,17 @@ module ActiveRecord module Associations # = Active Record Associations # - # This is the root class of all association proxies: + # This is the root class of all association proxies ('+ Foo' signifies an included module Foo): # # AssociationProxy # BelongsToAssociation - # HasOneAssociation # BelongsToPolymorphicAssociation - # AssociationCollection + # AssociationCollection + HasAssociation # HasAndBelongsToManyAssociation # HasManyAssociation - # HasManyThroughAssociation - # HasOneThroughAssociation + # HasManyThroughAssociation + ThroughAssociation + # HasOneAssociation + HasAssociation + # HasOneThroughAssociation + ThroughAssociation # # Association proxies in Active Record are middlemen between the object that # holds the association, known as the @owner, and the actual associated @@ -176,43 +176,6 @@ module ActiveRecord @reflection.klass.send(:sanitize_sql, sql, table_name) end - # Sets the owner attributes on the given record - # Note: does not really make sense for belongs_to associations, but this method is not - # used by belongs_to - def set_owner_attributes(record) - if @owner.persisted? - construct_owner_attributes.each { |key, value| record[key] = value } - end - end - - # Returns a has linking the owner to the association represented by the reflection - def construct_owner_attributes(reflection = @reflection) - attributes = {} - if reflection.macro == :belongs_to - attributes[reflection.association_primary_key] = @owner.send(reflection.primary_key_name) - else - attributes[reflection.primary_key_name] = @owner.send(reflection.active_record_primary_key) - - if reflection.options[:as] - attributes["#{reflection.options[:as]}_type"] = @owner.class.base_class.name - end - end - attributes - end - - # Builds an array of arel nodes from the owner attributes hash - def construct_owner_conditions(table = aliased_table, reflection = @reflection) - construct_owner_attributes(reflection).map do |attr, value| - table[attr].eq(value) - end - end - - def construct_conditions - conditions = construct_owner_conditions - conditions << Arel.sql(sql_conditions) if sql_conditions - aliased_table.create_and(conditions) - end - # Merges into +options+ the ones coming from the reflection. def merge_options_from_reflection!(options) options.reverse_merge!( @@ -312,18 +275,6 @@ module ActiveRecord end end - if RUBY_VERSION < '1.9.2' - # Array#flatten has problems with recursive arrays before Ruby 1.9.2. - # Going one level deeper solves the majority of the problems. - def flatten_deeper(array) - array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten - end - else - def flatten_deeper(array) - array.flatten - end - end - # Returns the ID of the owner, quoted if needed. def owner_quoted_id @owner.quoted_id diff --git a/activerecord/lib/active_record/associations/has_association.rb b/activerecord/lib/active_record/associations/has_association.rb new file mode 100644 index 0000000000..4407e2ea9a --- /dev/null +++ b/activerecord/lib/active_record/associations/has_association.rb @@ -0,0 +1,54 @@ +module ActiveRecord + module Associations + # Included in all has_* associations (i.e. everything except belongs_to) + module HasAssociation #:nodoc: + protected + # Sets the owner attributes on the given record + def set_owner_attributes(record) + if @owner.persisted? + construct_owner_attributes.each { |key, value| record[key] = value } + end + end + + # Returns a hash linking the owner to the association represented by the reflection + def construct_owner_attributes(reflection = @reflection) + attributes = {} + if reflection.macro == :belongs_to + attributes[reflection.association_primary_key] = @owner.send(reflection.primary_key_name) + else + attributes[reflection.primary_key_name] = @owner.send(reflection.active_record_primary_key) + + if reflection.options[:as] + attributes["#{reflection.options[:as]}_type"] = @owner.class.base_class.name + end + end + attributes + end + + # Builds an array of arel nodes from the owner attributes hash + def construct_owner_conditions(table = aliased_table, reflection = @reflection) + construct_owner_attributes(reflection).map do |attr, value| + table[attr].eq(value) + end + end + + def construct_conditions + conditions = construct_owner_conditions + conditions << Arel.sql(sql_conditions) if sql_conditions + aliased_table.create_and(conditions) + end + + if RUBY_VERSION < '1.9.2' + # Array#flatten has problems with recursive arrays before Ruby 1.9.2. + # Going one level deeper solves the majority of the problems. + def flatten_deeper(array) + array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten + end + else + def flatten_deeper(array) + array.flatten + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 4dabfe2ea3..8569a2f004 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -1,11 +1,10 @@ -require "active_record/associations/through_association_scope" require 'active_support/core_ext/object/blank' module ActiveRecord # = Active Record Has Many Through Association module Associations class HasManyThroughAssociation < HasManyAssociation #:nodoc: - include ThroughAssociationScope + include ThroughAssociation alias_method :new, :build diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 654157d998..ca0828ea7b 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -2,6 +2,8 @@ module ActiveRecord # = Active Record Belongs To Has One Association module Associations class HasOneAssociation < AssociationProxy #:nodoc: + include HasAssociation + def create(attrs = {}, replace_existing = true) new_record(replace_existing) do |reflection| attrs = merge_with_conditions(attrs) diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index 8c8c9fbe5d..c9ae930e93 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -1,10 +1,8 @@ -require "active_record/associations/through_association_scope" - module ActiveRecord # = Active Record Has One Through Association module Associations class HasOneThroughAssociation < HasOneAssociation - include ThroughAssociationScope + include ThroughAssociation def replace(new_value) create_through_record(new_value) diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb new file mode 100644 index 0000000000..57718285f8 --- /dev/null +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -0,0 +1,169 @@ +module ActiveRecord + # = Active Record Through Association + module Associations + module ThroughAssociation + + def scoped + with_scope(@scope) do + @reflection.klass.scoped & + @reflection.through_reflection.klass.scoped + end + end + + def stale_target? + if @target && @reflection.through_reflection.macro == :belongs_to && defined?(@through_foreign_key) + previous_key = @through_foreign_key.to_s + current_key = @owner.send(@reflection.through_reflection.primary_key_name).to_s + + previous_key != current_key + else + false + end + end + + protected + + def construct_find_scope + { + :conditions => construct_conditions, + :joins => construct_joins, + :include => @reflection.options[:include] || @reflection.source_reflection.options[:include], + :select => construct_select, + :order => @reflection.options[:order], + :limit => @reflection.options[:limit], + :readonly => @reflection.options[:readonly] + } + end + + # This scope affects the creation of the associated records (not the join records). At the + # moment we only support creating on a :through association when the source reflection is a + # belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so + # this scope has can legitimately be empty. + def construct_create_scope + { } + end + + def aliased_through_table + name = @reflection.through_reflection.table_name + + @reflection.table_name == name ? + @reflection.through_reflection.klass.arel_table.alias(name + "_join") : + @reflection.through_reflection.klass.arel_table + end + + def construct_owner_conditions + super(aliased_through_table, @reflection.through_reflection) + end + + def construct_select + @reflection.options[:select] || + @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*" + end + + def construct_joins + right = aliased_through_table + left = @reflection.klass.arel_table + + conditions = [] + + if @reflection.source_reflection.macro == :belongs_to + reflection_primary_key = @reflection.source_reflection.options[:primary_key] || + @reflection.klass.primary_key + source_primary_key = @reflection.source_reflection.primary_key_name + if @reflection.options[:source_type] + column = @reflection.source_reflection.options[:foreign_type] + conditions << + right[column].eq(@reflection.options[:source_type]) + end + else + reflection_primary_key = @reflection.source_reflection.primary_key_name + source_primary_key = @reflection.source_reflection.options[:primary_key] || + @reflection.through_reflection.klass.primary_key + if @reflection.source_reflection.options[:as] + column = "#{@reflection.source_reflection.options[:as]}_type" + conditions << + left[column].eq(@reflection.through_reflection.klass.name) + end + end + + conditions << + left[reflection_primary_key].eq(right[source_primary_key]) + + right.create_join( + right, + right.create_on(right.create_and(conditions))) + end + + # Construct attributes for :through pointing to owner and associate. + def construct_join_attributes(associate) + # TODO: revisit this to allow it for deletion, supposing dependent option is supported + raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro) + + join_attributes = { + @reflection.source_reflection.primary_key_name => + associate.send(@reflection.source_reflection.association_primary_key) + } + + if @reflection.options[:source_type] + join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name) + end + + if @reflection.through_reflection.options[:conditions].is_a?(Hash) + join_attributes.merge!(@reflection.through_reflection.options[:conditions]) + end + + join_attributes + end + + def conditions + @conditions = build_conditions unless defined?(@conditions) + @conditions + end + + def build_conditions + association_conditions = @reflection.options[:conditions] + through_conditions = build_through_conditions + source_conditions = @reflection.source_reflection.options[:conditions] + uses_sti = !@reflection.through_reflection.klass.descends_from_active_record? + + if association_conditions || through_conditions || source_conditions || uses_sti + all = [] + + [association_conditions, source_conditions].each do |conditions| + all << interpolate_sql(sanitize_sql(conditions)) if conditions + end + + all << through_conditions if through_conditions + all << build_sti_condition if uses_sti + + all.map { |sql| "(#{sql})" } * ' AND ' + end + end + + def build_through_conditions + conditions = @reflection.through_reflection.options[:conditions] + if conditions.is_a?(Hash) + interpolate_sql(@reflection.through_reflection.klass.send(:sanitize_sql, conditions)).gsub( + @reflection.quoted_table_name, + @reflection.through_reflection.quoted_table_name) + elsif conditions + interpolate_sql(sanitize_sql(conditions)) + end + end + + def build_sti_condition + @reflection.through_reflection.klass.send(:type_condition).to_sql + end + + alias_method :sql_conditions, :conditions + + def update_stale_state + construct_scope if stale_target? + + if @reflection.through_reflection.macro == :belongs_to + @through_foreign_key = @owner.send(@reflection.through_reflection.primary_key_name) + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb deleted file mode 100644 index 1abeb178ff..0000000000 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ /dev/null @@ -1,169 +0,0 @@ -module ActiveRecord - # = Active Record Through Association Scope - module Associations - module ThroughAssociationScope - - def scoped - with_scope(@scope) do - @reflection.klass.scoped & - @reflection.through_reflection.klass.scoped - end - end - - def stale_target? - if @target && @reflection.through_reflection.macro == :belongs_to && defined?(@through_foreign_key) - previous_key = @through_foreign_key.to_s - current_key = @owner.send(@reflection.through_reflection.primary_key_name).to_s - - previous_key != current_key - else - false - end - end - - protected - - def construct_find_scope - { - :conditions => construct_conditions, - :joins => construct_joins, - :include => @reflection.options[:include] || @reflection.source_reflection.options[:include], - :select => construct_select, - :order => @reflection.options[:order], - :limit => @reflection.options[:limit], - :readonly => @reflection.options[:readonly] - } - end - - # This scope affects the creation of the associated records (not the join records). At the - # moment we only support creating on a :through association when the source reflection is a - # belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so - # this scope has can legitimately be empty. - def construct_create_scope - { } - end - - def aliased_through_table - name = @reflection.through_reflection.table_name - - @reflection.table_name == name ? - @reflection.through_reflection.klass.arel_table.alias(name + "_join") : - @reflection.through_reflection.klass.arel_table - end - - def construct_owner_conditions - super(aliased_through_table, @reflection.through_reflection) - end - - def construct_select - @reflection.options[:select] || - @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*" - end - - def construct_joins - right = aliased_through_table - left = @reflection.klass.arel_table - - conditions = [] - - if @reflection.source_reflection.macro == :belongs_to - reflection_primary_key = @reflection.source_reflection.options[:primary_key] || - @reflection.klass.primary_key - source_primary_key = @reflection.source_reflection.primary_key_name - if @reflection.options[:source_type] - column = @reflection.source_reflection.options[:foreign_type] - conditions << - right[column].eq(@reflection.options[:source_type]) - end - else - reflection_primary_key = @reflection.source_reflection.primary_key_name - source_primary_key = @reflection.source_reflection.options[:primary_key] || - @reflection.through_reflection.klass.primary_key - if @reflection.source_reflection.options[:as] - column = "#{@reflection.source_reflection.options[:as]}_type" - conditions << - left[column].eq(@reflection.through_reflection.klass.name) - end - end - - conditions << - left[reflection_primary_key].eq(right[source_primary_key]) - - right.create_join( - right, - right.create_on(right.create_and(conditions))) - end - - # Construct attributes for :through pointing to owner and associate. - def construct_join_attributes(associate) - # TODO: revisit this to allow it for deletion, supposing dependent option is supported - raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro) - - join_attributes = { - @reflection.source_reflection.primary_key_name => - associate.send(@reflection.source_reflection.association_primary_key) - } - - if @reflection.options[:source_type] - join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name) - end - - if @reflection.through_reflection.options[:conditions].is_a?(Hash) - join_attributes.merge!(@reflection.through_reflection.options[:conditions]) - end - - join_attributes - end - - def conditions - @conditions = build_conditions unless defined?(@conditions) - @conditions - end - - def build_conditions - association_conditions = @reflection.options[:conditions] - through_conditions = build_through_conditions - source_conditions = @reflection.source_reflection.options[:conditions] - uses_sti = !@reflection.through_reflection.klass.descends_from_active_record? - - if association_conditions || through_conditions || source_conditions || uses_sti - all = [] - - [association_conditions, source_conditions].each do |conditions| - all << interpolate_sql(sanitize_sql(conditions)) if conditions - end - - all << through_conditions if through_conditions - all << build_sti_condition if uses_sti - - all.map { |sql| "(#{sql})" } * ' AND ' - end - end - - def build_through_conditions - conditions = @reflection.through_reflection.options[:conditions] - if conditions.is_a?(Hash) - interpolate_sql(@reflection.through_reflection.klass.send(:sanitize_sql, conditions)).gsub( - @reflection.quoted_table_name, - @reflection.through_reflection.quoted_table_name) - elsif conditions - interpolate_sql(sanitize_sql(conditions)) - end - end - - def build_sti_condition - @reflection.through_reflection.klass.send(:type_condition).to_sql - end - - alias_method :sql_conditions, :conditions - - def update_stale_state - construct_scope if stale_target? - - if @reflection.through_reflection.macro == :belongs_to - @through_foreign_key = @owner.send(@reflection.through_reflection.primary_key_name) - end - end - end - end -end -- cgit v1.2.3