aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations
diff options
context:
space:
mode:
authorJon Leighton <j@jonathanleighton.com>2011-02-21 01:09:41 +0000
committerAaron Patterson <aaron.patterson@gmail.com>2011-02-21 10:16:15 -0800
commit52f8e4b9dae6137fcd95793dffc26ddff80b623e (patch)
tree11a80259a618472810155ca46fd826246aab9906 /activerecord/lib/active_record/associations
parenta5274bb52c058bae69476bee3c95f472513a5725 (diff)
downloadrails-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')
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb53
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb83
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb75
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb63
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb63
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb61
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb32
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb39
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb1
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb19
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb16
11 files changed, 501 insertions, 4 deletions
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
new file mode 100644
index 0000000000..96fca97440
--- /dev/null
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -0,0 +1,53 @@
+module ActiveRecord::Associations::Builder
+ class Association #:nodoc:
+ class_attribute :valid_options
+ self.valid_options = [:class_name, :foreign_key, :select, :conditions, :include, :extend, :readonly, :validate]
+
+ # Set by subclasses
+ class_attribute :macro
+
+ attr_reader :model, :name, :options, :reflection
+
+ def self.build(model, name, options)
+ new(model, name, options).build
+ end
+
+ def initialize(model, name, options)
+ @model, @name, @options = model, name, options
+ end
+
+ def build
+ validate_options
+ reflection = model.create_reflection(self.class.macro, name, options, model)
+ define_accessors
+ reflection
+ end
+
+ private
+
+ def validate_options
+ options.assert_valid_keys(self.class.valid_options)
+ end
+
+ def define_accessors
+ define_readers
+ define_writers
+ end
+
+ def define_readers
+ name = self.name
+
+ model.redefine_method(name) do |*params|
+ association(name).reader(*params)
+ end
+ end
+
+ def define_writers
+ name = self.name
+
+ model.redefine_method("#{name}=") do |value|
+ association(name).writer(value)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
new file mode 100644
index 0000000000..964e7fddc8
--- /dev/null
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -0,0 +1,83 @@
+module ActiveRecord::Associations::Builder
+ class BelongsTo < SingularAssociation #:nodoc:
+ self.macro = :belongs_to
+
+ self.valid_options += [:foreign_type, :polymorphic, :touch]
+
+ def constructable?
+ !options[:polymorphic]
+ end
+
+ def build
+ reflection = super
+ add_counter_cache_callbacks(reflection) if options[:counter_cache]
+ add_touch_callbacks(reflection) if options[:touch]
+ configure_dependency
+ reflection
+ end
+
+ private
+
+ def add_counter_cache_callbacks(reflection)
+ cache_column = reflection.counter_cache_column
+ name = self.name
+
+ method_name = "belongs_to_counter_cache_after_create_for_#{name}"
+ model.redefine_method(method_name) do
+ record = send(name)
+ record.class.increment_counter(cache_column, record.id) unless record.nil?
+ end
+ model.after_create(method_name)
+
+ method_name = "belongs_to_counter_cache_before_destroy_for_#{name}"
+ model.redefine_method(method_name) do
+ record = send(name)
+ record.class.decrement_counter(cache_column, record.id) unless record.nil?
+ end
+ model.before_destroy(method_name)
+
+ model.send(: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)
+ name = self.name
+ method_name = "belongs_to_touch_after_save_or_destroy_for_#{name}"
+ touch = options[:touch]
+
+ model.redefine_method(method_name) do
+ record = send(name)
+
+ unless record.nil?
+ if touch == true
+ record.touch
+ else
+ record.touch(touch)
+ end
+ end
+ end
+
+ model.after_save(method_name)
+ model.after_touch(method_name)
+ model.after_destroy(method_name)
+ end
+
+ def configure_dependency
+ if options[:dependent]
+ unless [:destroy, :delete].include?(options[:dependent])
+ raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{options[:dependent].inspect})"
+ end
+
+ method_name = "belongs_to_dependent_#{options[:dependent]}_for_#{name}"
+ model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1)
+ def #{method_name}
+ association = #{name}
+ association.#{options[:dependent]} if association
+ end
+ eoruby
+ model.after_destroy method_name
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
new file mode 100644
index 0000000000..f62209a226
--- /dev/null
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -0,0 +1,75 @@
+module ActiveRecord::Associations::Builder
+ class CollectionAssociation < Association #:nodoc:
+ CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
+
+ self.valid_options += [
+ :table_name, :order, :group, :having, :limit, :offset, :uniq, :finder_sql,
+ :counter_sql, :before_add, :after_add, :before_remove, :after_remove
+ ]
+
+ attr_reader :block_extension
+
+ def self.build(model, name, options, &extension)
+ new(model, name, options, &extension).build
+ end
+
+ def initialize(model, name, options, &extension)
+ super(model, name, options)
+ @block_extension = extension
+ end
+
+ def build
+ wrap_block_extension
+ reflection = super
+ CALLBACKS.each { |callback_name| define_callback(callback_name) }
+ reflection
+ end
+
+ def writable?
+ true
+ end
+
+ private
+
+ def wrap_block_extension
+ options[:extend] = Array.wrap(options[:extend])
+
+ if block_extension
+ silence_warnings do
+ model.parent.const_set(extension_module_name, Module.new(&block_extension))
+ end
+ options[:extend].push("#{model.parent}::#{extension_module_name}".constantize)
+ end
+ end
+
+ def extension_module_name
+ @extension_module_name ||= "#{model.to_s.demodulize}#{name.to_s.camelize}AssociationExtension"
+ end
+
+ def define_callback(callback_name)
+ full_callback_name = "#{callback_name}_for_#{name}"
+
+ # TODO : why do i need method_defined? I think its because of the inheritance chain
+ model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name)
+ model.send("#{full_callback_name}=", Array.wrap(options[callback_name.to_sym]))
+ end
+
+ def define_readers
+ super
+
+ name = self.name
+ model.redefine_method("#{name.to_s.singularize}_ids") do
+ association(name).ids_reader
+ end
+ end
+
+ def define_writers
+ super
+
+ name = self.name
+ model.redefine_method("#{name.to_s.singularize}_ids=") do |ids|
+ association(name).ids_writer(ids)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
new file mode 100644
index 0000000000..e40b32826a
--- /dev/null
+++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
@@ -0,0 +1,63 @@
+module ActiveRecord::Associations::Builder
+ class HasAndBelongsToMany < CollectionAssociation #:nodoc:
+ self.macro = :has_and_belongs_to_many
+
+ self.valid_options += [:join_table, :association_foreign_key, :delete_sql, :insert_sql]
+
+ def build
+ reflection = super
+ check_validity(reflection)
+ redefine_destroy
+ reflection
+ end
+
+ private
+
+ def redefine_destroy
+ # Don't use a before_destroy callback since users' before_destroy
+ # callbacks will be executed after the association is wiped out.
+ name = self.name
+ model.send(:include, Module.new {
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def destroy # def destroy
+ super # super
+ #{name}.clear # posts.clear
+ end # end
+ RUBY
+ })
+ end
+
+ # TODO: These checks should probably be moved into the Reflection, and we should not be
+ # redefining the options[:join_table] value - instead we should define a
+ # reflection.join_table method.
+ def check_validity(reflection)
+ if reflection.association_foreign_key == reflection.foreign_key
+ raise ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection)
+ end
+
+ reflection.options[:join_table] ||= join_table_name(
+ model.send(:undecorated_table_name, model.to_s),
+ model.send(:undecorated_table_name, reflection.class_name)
+ )
+
+ if model.connection.supports_primary_key? && (model.connection.primary_key(reflection.options[:join_table]) rescue false)
+ raise ActiveRecord::HasAndBelongsToManyAssociationWithPrimaryKeyError.new(reflection)
+ end
+ end
+
+ # 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
+
+ model.table_name_prefix + join_table + model.table_name_suffix
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb
new file mode 100644
index 0000000000..77bb66228d
--- /dev/null
+++ b/activerecord/lib/active_record/associations/builder/has_many.rb
@@ -0,0 +1,63 @@
+module ActiveRecord::Associations::Builder
+ class HasMany < CollectionAssociation #:nodoc:
+ self.macro = :has_many
+
+ self.valid_options += [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of]
+
+ def build
+ reflection = super
+ configure_dependency
+ reflection
+ end
+
+ private
+
+ def configure_dependency
+ if options[:dependent]
+ unless [:destroy, :delete_all, :nullify, :restrict].include?(options[:dependent])
+ raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, " \
+ ":nullify or :restrict (#{options[:dependent].inspect})"
+ end
+
+ send("define_#{options[:dependent]}_dependency_method")
+ model.before_destroy dependency_method_name
+ end
+ end
+
+ def define_destroy_dependency_method
+ name = self.name
+ model.send(:define_method, dependency_method_name) do
+ send(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
+
+ send(name).delete_all
+ end
+ end
+
+ def define_delete_all_dependency_method
+ name = self.name
+ model.send(:define_method, dependency_method_name) do
+ send(name).delete_all
+ end
+ end
+ alias :define_nullify_dependency_method :define_delete_all_dependency_method
+
+ def define_restrict_dependency_method
+ name = self.name
+ model.send(:define_method, dependency_method_name) do
+ raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).empty?
+ end
+ end
+
+ def dependency_method_name
+ "has_many_dependent_for_#{name}"
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
new file mode 100644
index 0000000000..07ba5d088e
--- /dev/null
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -0,0 +1,61 @@
+module ActiveRecord::Associations::Builder
+ class HasOne < SingularAssociation #:nodoc:
+ self.macro = :has_one
+
+ self.valid_options += [:order, :as]
+
+ class_attribute :through_options
+ self.through_options = [:through, :source, :source_type]
+
+ def constructable?
+ !options[:through]
+ end
+
+ def build
+ reflection = super
+ configure_dependency unless options[:through]
+ reflection
+ end
+
+ private
+
+ def validate_options
+ valid_options = self.class.valid_options
+ valid_options += self.class.through_options if options[:through]
+ options.assert_valid_keys(valid_options)
+ end
+
+ def configure_dependency
+ if options[:dependent]
+ unless [:destroy, :delete, :nullify, :restrict].include?(options[:dependent])
+ raise ArgumentError, "The :dependent option expects either :destroy, :delete, " \
+ ":nullify or :restrict (#{options[:dependent].inspect})"
+ end
+
+ send("define_#{options[:dependent]}_dependency_method")
+ model.before_destroy dependency_method_name
+ end
+ end
+
+ def dependency_method_name
+ "has_one_dependent_#{options[:dependent]}_for_#{name}"
+ end
+
+ def define_destroy_dependency_method
+ model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1)
+ def #{dependency_method_name}
+ association(#{name.to_sym.inspect}).delete
+ end
+ eoruby
+ end
+ alias :define_delete_dependency_method :define_destroy_dependency_method
+ alias :define_nullify_dependency_method :define_destroy_dependency_method
+
+ def define_restrict_dependency_method
+ name = self.name
+ model.redefine_method(dependency_method_name) do
+ raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).nil?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
new file mode 100644
index 0000000000..06a414b874
--- /dev/null
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -0,0 +1,32 @@
+module ActiveRecord::Associations::Builder
+ class SingularAssociation < Association #:nodoc:
+ self.valid_options += [:remote, :dependent, :counter_cache, :primary_key, :inverse_of]
+
+ def constructable?
+ true
+ end
+
+ def define_accessors
+ super
+ define_constructors if constructable?
+ end
+
+ private
+
+ def define_constructors
+ name = self.name
+
+ model.redefine_method("build_#{name}") do |*params|
+ association(name).build(*params)
+ end
+
+ model.redefine_method("create_#{name}") do |*params|
+ association(name).create(*params)
+ end
+
+ model.redefine_method("create_#{name}!") do |*params|
+ association(name).create!(*params)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index ba98542b67..f3761bd2c7 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -32,6 +32,45 @@ module ActiveRecord
@proxy = CollectionProxy.new(self)
end
+ # Implements the reader method, e.g. foo.items for Foo.has_many :items
+ def reader(force_reload = false)
+ if force_reload
+ klass.uncached { reload }
+ elsif stale_target?
+ reload
+ end
+
+ proxy
+ end
+
+ # Implements the writer method, e.g. foo.items= for Foo.has_many :items
+ def writer(records)
+ replace(records)
+ end
+
+ # Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items
+ def ids_reader
+ if loaded? || options[:finder_sql]
+ load_target.map do |record|
+ record.send(reflection.association_primary_key)
+ end
+ else
+ column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
+
+ scoped.select(column).except(:includes).map! do |record|
+ record.send(reflection.association_primary_key)
+ end
+ end
+ end
+
+ # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
+ def ids_writer(ids)
+ pk_column = reflection.primary_key_column
+ ids = Array.wrap(ids).reject { |id| id.blank? }
+ ids.map! { |i| pk_column.type_cast(i) }
+ replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids))
+ end
+
def reset
@loaded = false
@target = []
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 5ae81649c5..cebf3e477a 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -6,6 +6,7 @@ module ActiveRecord
# If the association has a <tt>:through</tt> option further specialization
# is provided by its child HasManyThroughAssociation.
class HasManyAssociation < CollectionAssociation #:nodoc:
+
def insert_record(record, validate = true)
set_owner_attributes(record)
record.save(:validate => validate)
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 47d0042519..e13f97125f 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -26,11 +26,22 @@ module ActiveRecord
self.target = record
end
- protected
-
- def association_scope
- super.order(options[:order])
+ def delete(method = options[:dependent])
+ if load_target
+ case method
+ when :delete
+ target.delete
+ when :destroy
+ target.destroy
+ when :nullify
+ target.update_attribute(reflection.foreign_key, nil)
+ end
end
+ end
+
+ def association_scope
+ super.order(options[:order])
+ end
private
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index e75fdaf36f..0d8e45adb5 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -1,6 +1,22 @@
module ActiveRecord
module Associations
class SingularAssociation < Association #:nodoc:
+ # Implements the reader method, e.g. foo.bar for Foo.has_one :bar
+ def reader(force_reload = false)
+ if force_reload
+ klass.uncached { reload }
+ elsif !loaded? || stale_target?
+ reload
+ end
+
+ target
+ end
+
+ # Implements the writer method, e.g. foo.items= for Foo.has_many :items
+ def writer(record)
+ replace(record)
+ end
+
def create(attributes = {})
new_record(:create, attributes)
end