forked from rails/rails
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use proper objects to do the work to build the associations (adding m…
…ethods, callbacks etc) rather than calling a whole bunch of methods with rather long names.
- Loading branch information
1 parent
a5274bb
commit 52f8e4b
Showing
15 changed files
with
553 additions
and
418 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
53 changes: 53 additions & 0 deletions
53
activerecord/lib/active_record/associations/builder/association.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
83
activerecord/lib/active_record/associations/builder/belongs_to.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
75 changes: 75 additions & 0 deletions
75
activerecord/lib/active_record/associations/builder/collection_association.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
63 changes: 63 additions & 0 deletions
63
activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
63
activerecord/lib/active_record/associations/builder/has_many.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.