diff options
author | Jon Leighton <j@jonathanleighton.com> | 2011-02-21 01:09:41 +0000 |
---|---|---|
committer | Aaron Patterson <aaron.patterson@gmail.com> | 2011-02-21 10:16:15 -0800 |
commit | 52f8e4b9dae6137fcd95793dffc26ddff80b623e (patch) | |
tree | 11a80259a618472810155ca46fd826246aab9906 /activerecord/lib/active_record/associations.rb | |
parent | a5274bb52c058bae69476bee3c95f472513a5725 (diff) | |
download | rails-52f8e4b9dae6137fcd95793dffc26ddff80b623e.tar.gz rails-52f8e4b9dae6137fcd95793dffc26ddff80b623e.tar.bz2 rails-52f8e4b9dae6137fcd95793dffc26ddff80b623e.zip |
Use proper objects to do the work to build the associations (adding methods, callbacks etc) rather than calling a whole bunch of methods with rather long names.
Diffstat (limited to 'activerecord/lib/active_record/associations.rb')
-rw-r--r-- | activerecord/lib/active_record/associations.rb | 409 |
1 files changed, 22 insertions, 387 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 364a7248d2..e0b4db498d 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -107,8 +107,8 @@ module ActiveRecord # (has_many, has_one) when there is at least 1 child associated instance. # ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project class DeleteRestrictionError < ActiveRecordError #:nodoc: - def initialize(reflection) - super("Cannot delete record because of dependent #{reflection.name}") + def initialize(name) + super("Cannot delete record because of dependent #{name}") end end @@ -132,6 +132,17 @@ module ActiveRecord autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association' autoload :ThroughAssociation, 'active_record/associations/through_association' + module Builder #:nodoc: + autoload :Association, 'active_record/associations/builder/association' + autoload :SingularAssociation, 'active_record/associations/builder/singular_association' + autoload :CollectionAssociation, 'active_record/associations/builder/collection_association' + + autoload :BelongsTo, 'active_record/associations/builder/belongs_to' + autoload :HasOne, 'active_record/associations/builder/has_one' + autoload :HasMany, 'active_record/associations/builder/has_many' + autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many' + end + # Clears out the association cache. def clear_association_cache #:nodoc: @association_cache.clear if persisted? @@ -156,7 +167,7 @@ module ActiveRecord private # Returns the specified association instance if it responds to :loaded?, nil otherwise. def association_instance_get(name) - @association_cache[name] + @association_cache[name.to_sym] end # Set the specified association instance. @@ -1108,11 +1119,8 @@ module ActiveRecord # 'FROM people p, post_subscriptions ps ' + # 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' + # 'ORDER BY p.first_name' - def has_many(association_id, options = {}, &extension) - reflection = create_has_many_reflection(association_id, options, &extension) - configure_dependency_for_has_many(reflection) - add_association_callbacks(reflection.name, reflection.options) - collection_accessor_methods(reflection) + def has_many(name, options = {}, &extension) + Builder::HasMany.build(self, name, options, &extension) end # Specifies a one-to-one association with another class. This method should only be used @@ -1224,15 +1232,8 @@ module ActiveRecord # has_one :boss, :readonly => :true # has_one :club, :through => :membership # has_one :primary_address, :through => :addressables, :conditions => ["addressable.primary = ?", true], :source => :addressable - def has_one(association_id, options = {}) - if options[:through] - reflection = create_has_one_through_reflection(association_id, options) - else - reflection = create_has_one_reflection(association_id, options) - association_constructor_methods(reflection) - configure_dependency_for_has_one(reflection) - end - association_accessor_methods(reflection) + def has_one(name, options = {}) + Builder::HasOne.build(self, name, options) end # Specifies a one-to-one association with another class. This method should only be used @@ -1350,19 +1351,8 @@ module ActiveRecord # belongs_to :post, :counter_cache => true # belongs_to :company, :touch => true # belongs_to :company, :touch => :employees_last_updated_at - def belongs_to(association_id, options = {}) - reflection = create_belongs_to_reflection(association_id, options) - - association_accessor_methods(reflection) - - unless reflection.options[:polymorphic] - association_constructor_methods(reflection) - end - - add_counter_cache_callbacks(reflection) if options[:counter_cache] - add_touch_callbacks(reflection, options[:touch]) if options[:touch] - - configure_dependency_for_belongs_to(reflection) + def belongs_to(name, options = {}) + Builder::BelongsTo.build(self, name, options) end # Specifies a many-to-many relationship with another class. This associates two classes via an @@ -1533,364 +1523,9 @@ module ActiveRecord # has_and_belongs_to_many :categories, :readonly => true # has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql => # 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}' - def has_and_belongs_to_many(association_id, options = {}, &extension) - reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension) - collection_accessor_methods(reflection) - - # Don't use a before_destroy callback since users' before_destroy - # callbacks will be executed after the association is wiped out. - include Module.new { - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def destroy # def destroy - super # super - #{reflection.name}.clear # posts.clear - end # end - RUBY - } - - add_association_callbacks(reflection.name, options) + def has_and_belongs_to_many(name, options = {}, &extension) + Builder::HasAndBelongsToMany.build(self, name, options, &extension) end - - private - # Generates a join table name from two provided table names. - # The names in the join table names end up in lexicographic order. - # - # join_table_name("members", "clubs") # => "clubs_members" - # join_table_name("members", "special_clubs") # => "members_special_clubs" - def join_table_name(first_table_name, second_table_name) - if first_table_name < second_table_name - join_table = "#{first_table_name}_#{second_table_name}" - else - join_table = "#{second_table_name}_#{first_table_name}" - end - - table_name_prefix + join_table + table_name_suffix - end - - def association_accessor_methods(reflection) - redefine_method(reflection.name) do |*params| - force_reload = params.first unless params.empty? - association = association(reflection.name) - - if force_reload - reflection.klass.uncached { association.reload } - elsif !association.loaded? || association.stale_target? - association.reload - end - - association.target - end - - redefine_method("#{reflection.name}=") do |record| - association(reflection.name).replace(record) - end - end - - def collection_reader_method(reflection) - redefine_method(reflection.name) do |*params| - force_reload = params.first unless params.empty? - association = association(reflection.name) - - if force_reload - reflection.klass.uncached { association.reload } - elsif association.stale_target? - association.reload - end - - association.proxy - end - - redefine_method("#{reflection.name.to_s.singularize}_ids") do - if send(reflection.name).loaded? || reflection.options[:finder_sql] - records = send(reflection.name) - records.map { |r| r.send(reflection.association_primary_key) } - else - column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" - records = send(reflection.name).select(column).except(:includes) - records.map! { |r| r.send(reflection.association_primary_key) } - end - end - end - - def collection_accessor_methods(reflection, writer = true) - collection_reader_method(reflection) - - if writer - redefine_method("#{reflection.name}=") do |new_value| - association(reflection.name).replace(new_value) - end - - redefine_method("#{reflection.name.to_s.singularize}_ids=") do |new_value| - pk_column = reflection.primary_key_column - ids = (new_value || []).reject { |nid| nid.blank? } - ids.map!{ |i| pk_column.type_cast(i) } - send("#{reflection.name}=", reflection.klass.find(ids).index_by{ |r| r.id }.values_at(*ids)) - end - end - end - - def association_constructor_methods(reflection) - constructors = { - "build_#{reflection.name}" => "build", - "create_#{reflection.name}" => "create", - "create_#{reflection.name}!" => "create!" - } - - constructors.each do |name, proxy_name| - redefine_method(name) do |*params| - attributes = params.first unless params.empty? - association(reflection.name).send(proxy_name, attributes) - end - end - end - - def add_counter_cache_callbacks(reflection) - cache_column = reflection.counter_cache_column - - method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym - define_method(method_name) do - association = send(reflection.name) - association.class.increment_counter(cache_column, association.id) unless association.nil? - end - after_create(method_name) - - method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym - define_method(method_name) do - association = send(reflection.name) - association.class.decrement_counter(cache_column, association.id) unless association.nil? - end - before_destroy(method_name) - - module_eval( - "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)", __FILE__, __LINE__ - ) - end - - def add_touch_callbacks(reflection, touch_attribute) - method_name = :"belongs_to_touch_after_save_or_destroy_for_#{reflection.name}" - redefine_method(method_name) do - association = send(reflection.name) - - if touch_attribute == true - association.touch unless association.nil? - else - association.touch(touch_attribute) unless association.nil? - end - end - after_save(method_name) - after_touch(method_name) - after_destroy(method_name) - end - - # Creates before_destroy callback methods that nullify, delete or destroy - # has_many associated objects, according to the defined :dependent rule. - # - # See HasManyAssociation#delete_records for more information. In general - # - delete children if the option is set to :destroy or :delete_all - # - set the foreign key to NULL if the option is set to :nullify - # - do not delete the parent record if there is any child record if the - # option is set to :restrict - def configure_dependency_for_has_many(reflection) - if reflection.options[:dependent] - method_name = "has_many_dependent_for_#{reflection.name}" - - case reflection.options[:dependent] - when :destroy, :delete_all, :nullify - define_method(method_name) do - if reflection.options[:dependent] == :destroy - send(reflection.name).each do |o| - # No point in executing the counter update since we're going to destroy the parent anyway - counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym - if o.respond_to?(counter_method) - class << o - self - end.send(:define_method, counter_method, Proc.new {}) - end - end - end - - # AssociationProxy#delete_all looks at the :dependent option and acts accordingly - send(reflection.name).delete_all - end - when :restrict - define_method(method_name) do - unless send(reflection.name).empty? - raise DeleteRestrictionError.new(reflection) - end - end - else - raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, :nullify or :restrict (#{reflection.options[:dependent].inspect})" - end - - before_destroy method_name - end - end - - # Creates before_destroy callback methods that nullify, delete or destroy - # has_one associated objects, according to the defined :dependent rule. - # If the association is marked as :dependent => :restrict, create a callback - # that prevents deleting entirely. - def configure_dependency_for_has_one(reflection) - if reflection.options.include?(:dependent) - name = reflection.options[:dependent] - method_name = :"has_one_dependent_#{name}_for_#{reflection.name}" - - case name - when :destroy, :delete - class_eval <<-eoruby, __FILE__, __LINE__ + 1 - def #{method_name} - association = #{reflection.name} - association.#{name} if association - end - eoruby - when :nullify - class_eval <<-eoruby, __FILE__, __LINE__ + 1 - def #{method_name} - association = #{reflection.name} - association.update_attribute(#{reflection.foreign_key.inspect}, nil) if association - end - eoruby - when :restrict - method_name = "has_one_dependent_restrict_for_#{reflection.name}".to_sym - define_method(method_name) do - unless send(reflection.name).nil? - raise DeleteRestrictionError.new(reflection) - end - end - before_destroy method_name - else - raise ArgumentError, "The :dependent option expects either :destroy, :delete, :nullify or :restrict (#{reflection.options[:dependent].inspect})" - end - - before_destroy method_name - end - end - - def configure_dependency_for_belongs_to(reflection) - if reflection.options.include?(:dependent) - name = reflection.options[:dependent] - - unless [:destroy, :delete].include?(name) - raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{reflection.options[:dependent].inspect})" - end - - method_name = :"belongs_to_dependent_#{name}_for_#{reflection.name}" - class_eval <<-eoruby, __FILE__, __LINE__ + 1 - def #{method_name} - association = #{reflection.name} - association.#{name} if association - end - eoruby - after_destroy method_name - end - end - - mattr_accessor :valid_keys_for_has_many_association - @@valid_keys_for_has_many_association = [ - :class_name, :table_name, :foreign_key, :primary_key, - :dependent, - :select, :conditions, :include, :order, :group, :having, :limit, :offset, - :as, :through, :source, :source_type, - :uniq, - :finder_sql, :counter_sql, - :before_add, :after_add, :before_remove, :after_remove, - :extend, :readonly, - :validate, :inverse_of - ] - - def create_has_many_reflection(association_id, options, &extension) - options.assert_valid_keys(valid_keys_for_has_many_association) - options[:extend] = create_extension_modules(association_id, extension, options[:extend]) - - create_reflection(:has_many, association_id, options, self) - end - - mattr_accessor :valid_keys_for_has_one_association - @@valid_keys_for_has_one_association = [ - :class_name, :foreign_key, :remote, :select, :conditions, :order, - :include, :dependent, :counter_cache, :extend, :as, :readonly, - :validate, :primary_key, :inverse_of - ] - - def create_has_one_reflection(association_id, options) - options.assert_valid_keys(valid_keys_for_has_one_association) - create_reflection(:has_one, association_id, options, self) - end - - def create_has_one_through_reflection(association_id, options) - options.assert_valid_keys( - :class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source, :source_type, :validate - ) - create_reflection(:has_one, association_id, options, self) - end - - mattr_accessor :valid_keys_for_belongs_to_association - @@valid_keys_for_belongs_to_association = [ - :class_name, :primary_key, :foreign_key, :foreign_type, :remote, :select, :conditions, - :include, :dependent, :counter_cache, :extend, :polymorphic, :readonly, - :validate, :touch, :inverse_of - ] - - def create_belongs_to_reflection(association_id, options) - options.assert_valid_keys(valid_keys_for_belongs_to_association) - create_reflection(:belongs_to, association_id, options, self) - end - - mattr_accessor :valid_keys_for_has_and_belongs_to_many_association - @@valid_keys_for_has_and_belongs_to_many_association = [ - :class_name, :table_name, :join_table, :foreign_key, :association_foreign_key, - :select, :conditions, :include, :order, :group, :having, :limit, :offset, - :uniq, - :finder_sql, :counter_sql, :delete_sql, :insert_sql, - :before_add, :after_add, :before_remove, :after_remove, - :extend, :readonly, - :validate - ] - - def create_has_and_belongs_to_many_reflection(association_id, options, &extension) - options.assert_valid_keys(valid_keys_for_has_and_belongs_to_many_association) - options[:extend] = create_extension_modules(association_id, extension, options[:extend]) - - reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self) - - if reflection.association_foreign_key == reflection.foreign_key - raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection) - end - - reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name)) - if connection.supports_primary_key? && (connection.primary_key(reflection.options[:join_table]) rescue false) - raise HasAndBelongsToManyAssociationWithPrimaryKeyError.new(reflection) - end - - reflection - end - - def add_association_callbacks(association_name, options) - callbacks = %w(before_add after_add before_remove after_remove) - callbacks.each do |callback_name| - full_callback_name = "#{callback_name}_for_#{association_name}" - defined_callbacks = options[callback_name.to_sym] - - full_callback_value = options.has_key?(callback_name.to_sym) ? [defined_callbacks].flatten : [] - - # TODO : why do i need method_defined? I think its because of the inheritance chain - class_attribute full_callback_name.to_sym unless method_defined?(full_callback_name) - self.send("#{full_callback_name}=", full_callback_value) - end - end - - def create_extension_modules(association_id, block_extension, extensions) - if block_extension - extension_module_name = "#{self.to_s.demodulize}#{association_id.to_s.camelize}AssociationExtension" - - silence_warnings do - self.parent.const_set(extension_module_name, Module.new(&block_extension)) - end - Array.wrap(extensions).push("#{self.parent}::#{extension_module_name}".constantize) - else - Array.wrap(extensions) - end - end end end end |