association methods are now generated in modules

Instead of generating association methods directly in the model
class, they are generated in an anonymous module which
is then included in the model class. There is one such module
for each association. The only subtlety is that the
generated_attributes_methods module (from ActiveModel) must
be forced to be included before association methods are created
so that attribute methods will not shadow association methods.
This commit is contained in:
Josh Susser
2011-11-14 22:57:15 -08:00
parent 8d1a2b3ecd
commit 7cba6a3784
9 changed files with 43 additions and 30 deletions

View File

@@ -6,7 +6,7 @@ module ActiveRecord::Associations::Builder
# Set by subclasses
class_attribute :macro
attr_reader :model, :name, :options, :reflection
attr_reader :model, :name, :options, :reflection, :mixin
def self.build(model, name, options)
new(model, name, options).build
@@ -14,6 +14,8 @@ module ActiveRecord::Associations::Builder
def initialize(model, name, options)
@model, @name, @options = model, name, options
@mixin = Module.new
@model.__send__(:include, @mixin)
end
def build
@@ -36,16 +38,14 @@ module ActiveRecord::Associations::Builder
def define_readers
name = self.name
model.redefine_method(name) do |*params|
mixin.send(:define_method, name) do |*params|
association(name).reader(*params)
end
end
def define_writers
name = self.name
model.redefine_method("#{name}=") do |value|
mixin.send(:define_method, "#{name}=") do |value|
association(name).writer(value)
end
end

View File

@@ -25,14 +25,14 @@ module ActiveRecord::Associations::Builder
name = self.name
method_name = "belongs_to_counter_cache_after_create_for_#{name}"
model.redefine_method(method_name) do
mixin.send(:define_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
mixin.send(:define_method, method_name) do
record = send(name)
record.class.decrement_counter(cache_column, record.id) unless record.nil?
end
@@ -48,7 +48,7 @@ module ActiveRecord::Associations::Builder
method_name = "belongs_to_touch_after_save_or_destroy_for_#{name}"
touch = options[:touch]
model.redefine_method(method_name) do
mixin.send(:define_method, method_name) do
record = send(name)
unless record.nil?

View File

@@ -58,7 +58,7 @@ module ActiveRecord::Associations::Builder
super
name = self.name
model.redefine_method("#{name.to_s.singularize}_ids") do
mixin.send(:define_method, "#{name.to_s.singularize}_ids") do
association(name).ids_reader
end
end
@@ -67,7 +67,7 @@ module ActiveRecord::Associations::Builder
super
name = self.name
model.redefine_method("#{name.to_s.singularize}_ids=") do |ids|
mixin.send(:define_method, "#{name.to_s.singularize}_ids=") do |ids|
association(name).ids_writer(ids)
end
end

View File

@@ -15,14 +15,10 @@ module ActiveRecord::Associations::Builder
def define_destroy_hook
name = self.name
model.send(:include, Module.new {
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def destroy_associations
association(#{name.to_sym.inspect}).delete_all
super
end
RUBY
})
mixin.send(:define_method, :destroy_associations) do
association(name).delete_all
super()
end
end
# TODO: These checks should probably be moved into the Reflection, and we should not be

View File

@@ -28,7 +28,7 @@ module ActiveRecord::Associations::Builder
def define_destroy_dependency_method
name = self.name
model.send(:define_method, dependency_method_name) do
mixin.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
@@ -45,7 +45,7 @@ module ActiveRecord::Associations::Builder
def define_delete_all_dependency_method
name = self.name
model.send(:define_method, dependency_method_name) do
mixin.send(:define_method, dependency_method_name) do
send(name).delete_all
end
end
@@ -53,7 +53,7 @@ module ActiveRecord::Associations::Builder
def define_restrict_dependency_method
name = self.name
model.send(:define_method, dependency_method_name) do
mixin.send(:define_method, dependency_method_name) do
raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).empty?
end
end

View File

@@ -44,18 +44,17 @@ module ActiveRecord::Associations::Builder
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
name = self.name
mixin.send(:define_method, dependency_method_name) do
association(name).delete
end
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
mixin.send(:define_method, dependency_method_name) do
raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).nil?
end
end

View File

@@ -16,15 +16,15 @@ module ActiveRecord::Associations::Builder
def define_constructors
name = self.name
model.redefine_method("build_#{name}") do |*params, &block|
mixin.send(:define_method, "build_#{name}") do |*params, &block|
association(name).build(*params, &block)
end
model.redefine_method("create_#{name}") do |*params, &block|
mixin.send(:define_method, "create_#{name}") do |*params, &block|
association(name).create(*params, &block)
end
model.redefine_method("create_#{name}!") do |*params, &block|
mixin.send(:define_method, "create_#{name}!") do |*params, &block|
association(name).create!(*params, &block)
end
end

View File

@@ -8,6 +8,12 @@ module ActiveRecord
include ActiveModel::AttributeMethods
module ClassMethods
def inherited(child_class)
# force creation + include before accessor method modules
child_class.generated_attribute_methods
super
end
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods

View File

@@ -1,4 +1,5 @@
require "cases/helper"
require 'models/computer'
require 'models/developer'
require 'models/project'
require 'models/company'
@@ -273,3 +274,14 @@ class OverridingAssociationsTest < ActiveRecord::TestCase
)
end
end
class GeneratedMethodsTest < ActiveRecord::TestCase
fixtures :developers, :computers
def test_association_methods_override_attribute_methods_of_same_name
assert_equal(developers(:david), computers(:workstation).developer)
# this next line will fail if the attribute methods module is generated lazily
# after the association methods module is generated
assert_equal(developers(:david), computers(:workstation).developer)
assert_equal(developers(:david).id, computers(:workstation)[:developer])
end
end