Skip to content

Commit

Permalink
Use proper objects to do the work to build the associations (adding m…
Browse files Browse the repository at this point in the history
…ethods, callbacks etc) rather than calling a whole bunch of methods with rather long names.
  • Loading branch information
jonleighton authored and tenderlove committed Feb 21, 2011
1 parent a5274bb commit 52f8e4b
Show file tree
Hide file tree
Showing 15 changed files with 553 additions and 418 deletions.
409 changes: 22 additions & 387 deletions activerecord/lib/active_record/associations.rb

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions activerecord/lib/active_record/associations/builder/association.rb
Original file line number Diff line number Diff line change
@@ -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
83 changes: 83 additions & 0 deletions activerecord/lib/active_record/associations/builder/belongs_to.rb
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
63 changes: 63 additions & 0 deletions activerecord/lib/active_record/associations/builder/has_many.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 52f8e4b

Please sign in to comment.