From ccea98389abbf150b886c9f964b1def47f00f237 Mon Sep 17 00:00:00 2001 From: Murray Steele Date: Fri, 1 May 2009 16:01:13 +0100 Subject: Providing support for :inverse_of as an option to associations. You can now add an :inverse_of option to has_one, has_many and belongs_to associations. This is best described with an example: class Man < ActiveRecord::Base has_one :face, :inverse_of => :man end class Face < ActiveRecord::Base belongs_to :man, :inverse_of => :face end m = Man.first f = m.face Without :inverse_of m and f.man would be different instances of the same object (f.man being pulled from the database again). With these new :inverse_of options m and f.man are the same in memory instance. Currently :inverse_of supports has_one and has_many (but not the :through variants) associations. It also supplies inverse support for belongs_to associations where the inverse is a has_one and it's not a polymorphic. Signed-off-by: Murray Steele Signed-off-by: Jeremy Kemper --- activerecord/lib/active_record/associations.rb | 12 +++++++++--- .../associations/association_collection.rb | 8 ++++++-- .../active_record/associations/association_proxy.rb | 14 ++++++++++++++ .../associations/belongs_to_association.rb | 12 +++++++++++- .../associations/has_many_association.rb | 5 +++++ .../associations/has_many_through_association.rb | 10 +++++----- .../associations/has_one_association.rb | 11 ++++++++++- activerecord/lib/active_record/reflection.rb | 21 +++++++++++++++++++++ 8 files changed, 81 insertions(+), 12 deletions(-) (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 2115878e32..0952b087d1 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1,4 +1,10 @@ module ActiveRecord + class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc: + def initialize(reflection) + super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{reflection.class_name})") + end + end + class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc: def initialize(owner_class_name, reflection) super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}") @@ -1488,7 +1494,7 @@ module ActiveRecord :finder_sql, :counter_sql, :before_add, :after_add, :before_remove, :after_remove, :extend, :readonly, - :validate + :validate, :inverse_of ] def create_has_many_reflection(association_id, options, &extension) @@ -1502,7 +1508,7 @@ module ActiveRecord @@valid_keys_for_has_one_association = [ :class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly, - :validate, :primary_key + :validate, :primary_key, :inverse_of ] def create_has_one_reflection(association_id, options) @@ -1521,7 +1527,7 @@ module ActiveRecord @@valid_keys_for_belongs_to_association = [ :class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, :include, :dependent, :counter_cache, :extend, :polymorphic, :readonly, - :validate, :touch + :validate, :touch, :inverse_of ] def create_belongs_to_reflection(association_id, options) diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 3aef1b21e9..26987dde97 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -399,11 +399,14 @@ module ActiveRecord find(:all) end - @reflection.options[:uniq] ? uniq(records) : records + records = @reflection.options[:uniq] ? uniq(records) : records + records.each do |record| + set_inverse_instance(record, @owner) + end + records end private - def create_record(attrs) attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) ensure_owner_is_not_new @@ -433,6 +436,7 @@ module ActiveRecord @target ||= [] unless loaded? @target << record unless @reflection.options[:uniq] && @target.include?(record) callback(:after_add, record) + set_inverse_instance(record, @owner) record end diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 241b9bfee0..e36b04ea95 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -53,6 +53,7 @@ module ActiveRecord def initialize(owner, reflection) @owner, @reflection = owner, reflection + reflection.check_validity! Array(reflection.options[:extend]).each { |ext| proxy_extend(ext) } reset end @@ -274,6 +275,19 @@ module ActiveRecord def owner_quoted_id @owner.quoted_id end + + def set_inverse_instance(record, instance) + return if record.nil? || !we_can_set_the_inverse_on_this?(record) + inverse_relationship = @reflection.inverse_of + unless inverse_relationship.nil? + record.send(:"set_#{inverse_relationship.name}_target", instance) + end + end + + # Override in subclasses + def we_can_set_the_inverse_on_this?(record) + false + end end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index f05c6be075..c88575048a 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -31,6 +31,8 @@ module ActiveRecord @updated = true end + set_inverse_instance(record, @owner) + loaded record end @@ -41,18 +43,26 @@ module ActiveRecord private def find_target - @reflection.klass.find( + the_target = @reflection.klass.find( @owner[@reflection.primary_key_name], :select => @reflection.options[:select], :conditions => conditions, :include => @reflection.options[:include], :readonly => @reflection.options[:readonly] ) + set_inverse_instance(the_target, @owner) + the_target end def foreign_key_present !@owner[@reflection.primary_key_name].nil? end + + # NOTE - for now, we're only supporting inverse setting from belongs_to back onto + # has_one associations. + def we_can_set_the_inverse_on_this?(record) + @reflection.has_inverse? && @reflection.inverse_of.macro == :has_one + end end end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index a2cbabfe0c..73dd50dd07 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -116,6 +116,11 @@ module ActiveRecord :create => create_scoping } end + + def we_can_set_the_inverse_on_this?(record) + inverse = @reflection.inverse_of + return !inverse.nil? + 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 1c091e7d5a..2dca84b911 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,6 @@ module ActiveRecord module Associations class HasManyThroughAssociation < HasManyAssociation #:nodoc: - def initialize(owner, reflection) - reflection.check_validity! - super - end - alias_method :new, :build def create!(attrs = nil) @@ -251,6 +246,11 @@ module ActiveRecord def cached_counter_attribute_name "#{@reflection.name}_count" end + + # NOTE - not sure that we can actually cope with inverses here + def we_can_set_the_inverse_on_this?(record) + false + end end end end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 1464227bb0..4908005d2e 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -74,13 +74,15 @@ module ActiveRecord private def find_target - @reflection.klass.find(:first, + the_target = @reflection.klass.find(:first, :conditions => @finder_sql, :select => @reflection.options[:select], :order => @reflection.options[:order], :include => @reflection.options[:include], :readonly => @reflection.options[:readonly] ) + set_inverse_instance(the_target, @owner) + the_target end def construct_sql @@ -117,8 +119,15 @@ module ActiveRecord self.target = record end + set_inverse_instance(record, @owner) + record end + + def we_can_set_the_inverse_on_this?(record) + inverse = @reflection.inverse_of + return !inverse.nil? + end end end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 2d4c1d5507..ec0175497d 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -212,6 +212,13 @@ module ActiveRecord end def check_validity! + check_validity_of_inverse! + end + + def check_validity_of_inverse! + if has_inverse? && inverse_of.nil? + raise InverseOfAssociationNotFoundError.new(self) + end end def through_reflection @@ -225,6 +232,18 @@ module ActiveRecord nil end + def has_inverse? + !@options[:inverse_of].nil? + end + + def inverse_of + if has_inverse? + @inverse_of ||= klass.reflect_on_association(options[:inverse_of]) + else + nil + end + end + private def derive_class_name class_name = name.to_s.camelize @@ -300,6 +319,8 @@ module ActiveRecord unless [:belongs_to, :has_many].include?(source_reflection.macro) && source_reflection.options[:through].nil? raise HasManyThroughSourceAssociationMacroError.new(self) end + + check_validity_of_inverse! end def through_reflection_primary_key -- cgit v1.2.3