1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Merge pull request #3636 from joshsusser/master

association methods are now generated in modules
This commit is contained in:
Jon Leighton 2011-11-29 10:09:42 -08:00
commit 2169603385
14 changed files with 112 additions and 23 deletions

View file

@ -1,5 +1,11 @@
## Rails 3.2.0 (unreleased) ##
* Generated association methods are created within a separate module to allow overriding and
composition using `super`. For a class named `MyModel`, the module is named
`MyModel::GeneratedFeatureMethods`. It is included into the model class immediately after
the `generated_attributes_methods` module defined in ActiveModel, so association methods
override attribute methods of the same name. *Josh Susser*
* Implemented ActiveRecord::Relation#explain. *fxn*
* Add ActiveRecord::Relation#uniq for generating unique queries.

View file

@ -196,6 +196,26 @@ module ActiveRecord
# * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt>
# <tt>Project#categories.delete(category1)</tt>
#
# === Overriding generated methods
#
# Association methods are generated in a module that is included into the model class,
# which allows you to easily override with your own methods and call the original
# generated method with +super+. For example:
#
# class Car < ActiveRecord::Base
# belongs_to :owner
# belongs_to :old_owner
# def owner=(new_owner)
# self.old_owner = self.owner
# super
# end
# end
#
# If your model class is <tt>Project</tt>, the module is
# named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is
# is included in the model class immediately after the (anonymous) generated attributes methods
# module, meaning an association will override the methods for an attribute with the same name.
#
# === A word of warning
#
# Don't create associations that have the same name as instance methods of

View file

@ -16,6 +16,10 @@ module ActiveRecord::Associations::Builder
@model, @name, @options = model, name, options
end
def mixin
@model.generated_feature_methods
end
def build
validate_options
reflection = model.create_reflection(self.class.macro, name, options, model)
@ -36,16 +40,14 @@ module ActiveRecord::Associations::Builder
def define_readers
name = self.name
model.redefine_method(name) do |*params|
mixin.redefine_method(name) do |*params|
association(name).reader(*params)
end
end
def define_writers
name = self.name
model.redefine_method("#{name}=") do |value|
mixin.redefine_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.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
mixin.redefine_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.redefine_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.redefine_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.redefine_method("#{name.to_s.singularize}_ids=") do |ids|
association(name).ids_writer(ids)
end
end

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.redefine_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.redefine_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.redefine_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.redefine_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.redefine_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.redefine_method("build_#{name}") do |*params, &block|
association(name).build(*params, &block)
end
model.redefine_method("create_#{name}") do |*params, &block|
mixin.redefine_method("create_#{name}") do |*params, &block|
association(name).create(*params, &block)
end
model.redefine_method("create_#{name}!") do |*params, &block|
mixin.redefine_method("create_#{name}!") do |*params, &block|
association(name).create!(*params, &block)
end
end

View file

@ -450,6 +450,20 @@ module ActiveRecord #:nodoc:
:having, :create_with, :uniq, :to => :scoped
delegate :count, :average, :minimum, :maximum, :sum, :calculate, :to => :scoped
def inherited(child_class) #:nodoc:
# force attribute methods to be higher in inheritance hierarchy than other generated methods
child_class.generated_attribute_methods
child_class.generated_feature_methods
super
end
def generated_feature_methods
unless const_defined?(:GeneratedFeatureMethods, false)
include const_set(:GeneratedFeatureMethods, Module.new)
end
const_get(:GeneratedFeatureMethods)
end
# Executes a custom SQL query against your database and returns all the results. The results will
# be returned as an array with columns requested encapsulated as attributes of the model you call
# this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in

View file

@ -77,7 +77,7 @@ end
class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects,
:parrots, :pirates, :treasures, :price_estimates, :tags, :taggings
:parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings
def setup_data_for_habtm_case
ActiveRecord::Base.connection.execute('delete from countries_treaties')
@ -445,6 +445,26 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert david.projects(true).empty?
end
def test_destroy_associations_destroys_multiple_associations
george = parrots(:george)
assert !george.pirates.empty?
assert !george.treasures.empty?
assert_no_difference "Pirate.count" do
assert_no_difference "Treasure.count" do
george.destroy_associations
end
end
join_records = Parrot.connection.select_all("SELECT * FROM parrots_pirates WHERE parrot_id = #{george.id}")
assert join_records.empty?
assert george.pirates(true).empty?
join_records = Parrot.connection.select_all("SELECT * FROM parrots_treasures WHERE parrot_id = #{george.id}")
assert join_records.empty?
assert george.treasures(true).empty?
end
def test_deprecated_push_with_attributes_was_removed
jamis = developers(:jamis)
assert_raise(NoMethodError) do

View file

@ -1,4 +1,5 @@
require "cases/helper"
require 'models/computer'
require 'models/developer'
require 'models/project'
require 'models/company'
@ -273,3 +274,18 @@ class OverridingAssociationsTest < ActiveRecord::TestCase
)
end
end
class GeneratedMethodsTest < ActiveRecord::TestCase
fixtures :developers, :computers, :posts, :comments
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
def test_model_method_overrides_association_method
assert_equal(comments(:greetings).body, posts(:welcome).first_comment)
end
end

View file

@ -69,6 +69,15 @@ end
class BasicsTest < ActiveRecord::TestCase
fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts
def test_generated_methods_modules
modules = Computer.ancestors
assert modules.include?(Computer::GeneratedFeatureMethods)
assert_equal(Computer::GeneratedFeatureMethods, Computer.generated_feature_methods)
assert(modules.index(Computer.generated_attribute_methods) > modules.index(Computer.generated_feature_methods),
"generated_attribute_methods must be higher in inheritance hierarchy than generated_feature_methods")
assert_not_equal Computer.generated_feature_methods, Post.generated_feature_methods
end
def test_column_names_are_escaped
conn = ActiveRecord::Base.connection
classname = conn.class.name[/[^:]*$/]

View file

@ -128,7 +128,6 @@ class Author < ActiveRecord::Base
belongs_to :author_address, :dependent => :destroy
belongs_to :author_address_extra, :dependent => :delete, :class_name => "AuthorAddress"
has_many :post_categories, :through => :posts, :source => :categories
has_many :category_post_comments, :through => :categories, :source => :post_comments
has_many :misc_posts, :class_name => 'Post',

View file

@ -24,6 +24,10 @@ class Post < ActiveRecord::Base
belongs_to :author_with_posts, :class_name => "Author", :foreign_key => :author_id, :include => :posts
belongs_to :author_with_address, :class_name => "Author", :foreign_key => :author_id, :include => :author_address
def first_comment
super.body
end
has_one :first_comment, :class_name => 'Comment', :order => 'id ASC'
has_one :last_comment, :class_name => 'Comment', :order => 'id desc'
scope :with_special_comments, :joins => :comments, :conditions => {:comments => {:type => 'SpecialComment'} }