mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
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.
This commit is contained in:
parent
a5274bb52c
commit
52f8e4b9da
15 changed files with 553 additions and 418 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 = []
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -116,30 +116,29 @@ module ActiveRecord
|
|||
module AutosaveAssociation
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
ASSOCIATION_TYPES = %w{ has_one belongs_to has_many has_and_belongs_to_many }
|
||||
ASSOCIATION_TYPES = %w{ HasOne HasMany BelongsTo HasAndBelongsToMany }
|
||||
|
||||
module AssociationBuilderExtension #:nodoc:
|
||||
def self.included(base)
|
||||
base.valid_options << :autosave
|
||||
end
|
||||
|
||||
def build
|
||||
reflection = super
|
||||
model.send(:add_autosave_association_callbacks, reflection)
|
||||
reflection
|
||||
end
|
||||
end
|
||||
|
||||
included do
|
||||
ASSOCIATION_TYPES.each do |type|
|
||||
send("valid_keys_for_#{type}_association") << :autosave
|
||||
Associations::Builder.const_get(type).send(:include, AssociationBuilderExtension)
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
private
|
||||
|
||||
# def belongs_to(name, options = {})
|
||||
# super
|
||||
# add_autosave_association_callbacks(reflect_on_association(name))
|
||||
# end
|
||||
ASSOCIATION_TYPES.each do |type|
|
||||
module_eval <<-CODE, __FILE__, __LINE__ + 1
|
||||
def #{type}(name, options = {})
|
||||
super
|
||||
add_autosave_association_callbacks(reflect_on_association(name))
|
||||
end
|
||||
CODE
|
||||
end
|
||||
|
||||
def define_non_cyclic_method(name, reflection, &block)
|
||||
define_method(name) do |*args|
|
||||
result = true; @_already_called ||= {}
|
||||
|
|
|
@ -29,7 +29,7 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase
|
|||
assert_equal projects(:action_controller), developers(:david).projects_extended_by_name_and_block.find_most_recent
|
||||
assert_equal projects(:active_record), developers(:david).projects_extended_by_name_and_block.find_least_recent
|
||||
end
|
||||
|
||||
|
||||
def test_extension_with_scopes
|
||||
assert_equal comments(:greetings), posts(:welcome).comments.offset(1).find_most_recent
|
||||
assert_equal comments(:greetings), posts(:welcome).comments.not_again.find_most_recent
|
||||
|
@ -52,12 +52,16 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_extension_name
|
||||
extension = Proc.new {}
|
||||
name = :association_name
|
||||
|
||||
assert_equal 'DeveloperAssociationNameAssociationExtension', Developer.send(:create_extension_modules, name, extension, []).first.name
|
||||
assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', MyApplication::Business::Developer.send(:create_extension_modules, name, extension, []).first.name
|
||||
assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', MyApplication::Business::Developer.send(:create_extension_modules, name, extension, []).first.name
|
||||
assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', MyApplication::Business::Developer.send(:create_extension_modules, name, extension, []).first.name
|
||||
assert_equal 'DeveloperAssociationNameAssociationExtension', extension_name(Developer)
|
||||
assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', extension_name(MyApplication::Business::Developer)
|
||||
assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', extension_name(MyApplication::Business::Developer)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def extension_name(model)
|
||||
builder = ActiveRecord::Associations::Builder::HasMany.new(model, :association_name, {}) { }
|
||||
builder.send(:wrap_block_extension)
|
||||
builder.options[:extend].first.name
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,19 +21,19 @@ require 'models/eye'
|
|||
|
||||
class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
|
||||
def test_autosave_should_be_a_valid_option_for_has_one
|
||||
assert base.valid_keys_for_has_one_association.include?(:autosave)
|
||||
assert ActiveRecord::Associations::Builder::HasOne.valid_options.include?(:autosave)
|
||||
end
|
||||
|
||||
def test_autosave_should_be_a_valid_option_for_belongs_to
|
||||
assert base.valid_keys_for_belongs_to_association.include?(:autosave)
|
||||
assert ActiveRecord::Associations::Builder::BelongsTo.valid_options.include?(:autosave)
|
||||
end
|
||||
|
||||
def test_autosave_should_be_a_valid_option_for_has_many
|
||||
assert base.valid_keys_for_has_many_association.include?(:autosave)
|
||||
assert ActiveRecord::Associations::Builder::HasMany.valid_options.include?(:autosave)
|
||||
end
|
||||
|
||||
def test_autosave_should_be_a_valid_option_for_has_and_belongs_to_many
|
||||
assert base.valid_keys_for_has_and_belongs_to_many_association.include?(:autosave)
|
||||
assert ActiveRecord::Associations::Builder::HasAndBelongsToMany.valid_options.include?(:autosave)
|
||||
end
|
||||
|
||||
def test_should_not_add_the_same_callbacks_multiple_times_for_has_one
|
||||
|
|
Loading…
Reference in a new issue