Skip to content

Commit

Permalink
association methods are now generated in modules
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
joshsusser committed Nov 16, 2011
1 parent 8d1a2b3 commit 7cba6a3
Show file tree
Hide file tree
Showing 9 changed files with 43 additions and 30 deletions.
Expand Up @@ -6,14 +6,16 @@ class Association #:nodoc:
# 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
end

def initialize(model, name, options)
@model, @name, @options = model, name, options
@mixin = Module.new
@model.__send__(:include, @mixin)
end

def build
Expand All @@ -36,16 +38,14 @@ def define_accessors

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
Expand Down
Expand Up @@ -25,14 +25,14 @@ def add_counter_cache_callbacks(reflection)
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
Expand All @@ -48,7 +48,7 @@ def add_touch_callbacks(reflection)
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?
Expand Down
Expand Up @@ -58,7 +58,7 @@ def define_readers
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
Expand All @@ -67,7 +67,7 @@ def define_writers
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
Expand Down
Expand Up @@ -15,14 +15,10 @@ def build

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
Expand Down
Expand Up @@ -28,7 +28,7 @@ def configure_dependency

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
Expand All @@ -45,15 +45,15 @@ class << o

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
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
mixin.send(:define_method, dependency_method_name) do
raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).empty?
end
end
Expand Down
11 changes: 5 additions & 6 deletions activerecord/lib/active_record/associations/builder/has_one.rb
Expand Up @@ -44,18 +44,17 @@ def dependency_method_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
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
Expand Down
Expand Up @@ -16,15 +16,15 @@ def define_accessors
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
Expand Down
6 changes: 6 additions & 0 deletions activerecord/lib/active_record/attribute_methods.rb
Expand Up @@ -8,6 +8,12 @@ module AttributeMethods #:nodoc:
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
Expand Down
12 changes: 12 additions & 0 deletions activerecord/test/cases/associations_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
require 'models/computer'
require 'models/developer'
require 'models/project'
require 'models/company'
Expand Down Expand Up @@ -273,3 +274,14 @@ def test_has_one_association_redefinition_reflections_should_differ_and_not_inhe
)
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

0 comments on commit 7cba6a3

Please sign in to comment.