mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Added callback hooks to association collections #1549 [Florian Weber]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1653 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
parent
555072e8e9
commit
4180e57b70
5 changed files with 137 additions and 5 deletions
|
@ -1,5 +1,18 @@
|
|||
*SVN*
|
||||
|
||||
* Added callback hooks to association collections #1549 [Florian Weber]. Example:
|
||||
|
||||
class Project
|
||||
has_and_belongs_to_many :developers, :before_add => :evaluate_velocity
|
||||
|
||||
def evaluate_velocity(developer)
|
||||
...
|
||||
end
|
||||
end
|
||||
|
||||
..raising an exception will cause the object not to be added (or removed, with before_remove).
|
||||
|
||||
|
||||
* Fixed Base.content_columns call for SQL Server adapter #1450 [DeLynn Berry]
|
||||
|
||||
* Fixed Base#write_attribute to work with both symbols and strings #1190 [Paul Legato]
|
||||
|
|
|
@ -96,6 +96,30 @@ module ActiveRecord
|
|||
# * You can add an object to a collection without automatically saving it by using the #collection.build method (documented below).
|
||||
# * All unsaved (new_record? == true) members of the collection are automatically saved when the parent is saved.
|
||||
#
|
||||
# === Callbacks
|
||||
#
|
||||
# Similiar to the normal callbacks that hook into the lifecycle of an Active Record object, you can also define callbacks that get
|
||||
# trigged when you add an object to or removing an object from a association collection. Example:
|
||||
#
|
||||
# class Project
|
||||
# has_and_belongs_to_many :developers, :after_add => :evaluate_velocity
|
||||
#
|
||||
# def evaluate_velocity(developer)
|
||||
# ...
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# It's possible to stack callbacks by passing them as an array. Example:
|
||||
#
|
||||
# class Project
|
||||
# has_and_belongs_to_many :developers, :after_add => [:evaluate_velocity, Proc.new {|project, developer| project.shipping_date = Time.now}]
|
||||
# end
|
||||
#
|
||||
# Possible callbacks are: before_add, after_add, before_remove and after_remove.
|
||||
#
|
||||
# Should any of the before_add callbacks throw an exception, the object does not get added to the collection. Same with
|
||||
# the before_remove callbacks, if an exception is thrown the object doesn't get removed.
|
||||
#
|
||||
# == Caching
|
||||
#
|
||||
# All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically
|
||||
|
@ -259,7 +283,8 @@ module ActiveRecord
|
|||
# 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
|
||||
# 'ORDER BY p.first_name'
|
||||
def has_many(association_id, options = {})
|
||||
validate_options([ :foreign_key, :class_name, :exclusively_dependent, :dependent, :conditions, :order, :finder_sql, :counter_sql ], options.keys)
|
||||
validate_options([ :foreign_key, :class_name, :exclusively_dependent, :dependent, :conditions, :order, :finder_sql, :counter_sql,
|
||||
:before_add, :after_add, :before_remove, :after_remove ], options.keys)
|
||||
association_name, association_class_name, association_class_primary_key_name =
|
||||
associate_identification(association_id, options[:class_name], options[:foreign_key])
|
||||
|
||||
|
@ -276,7 +301,8 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
add_multiple_associated_save_callbacks(association_name)
|
||||
|
||||
add_association_callbacks(association_name, options)
|
||||
|
||||
collection_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasManyAssociation)
|
||||
|
||||
# deprecated api
|
||||
|
@ -518,7 +544,8 @@ module ActiveRecord
|
|||
# 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}'
|
||||
def has_and_belongs_to_many(association_id, options = {})
|
||||
validate_options([ :class_name, :table_name, :foreign_key, :association_foreign_key, :conditions,
|
||||
:join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq ], options.keys)
|
||||
:join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq, :before_add, :after_add,
|
||||
:before_remove, :after_remove ], options.keys)
|
||||
association_name, association_class_name, association_class_primary_key_name =
|
||||
associate_identification(association_id, options[:class_name], options[:foreign_key])
|
||||
|
||||
|
@ -532,6 +559,7 @@ module ActiveRecord
|
|||
|
||||
before_destroy_sql = "DELETE FROM #{options[:join_table]} WHERE #{association_class_primary_key_name} = \\\#{self.quoted_id}"
|
||||
module_eval(%{before_destroy "self.connection.delete(%{#{before_destroy_sql}})"}) # "
|
||||
add_association_callbacks(association_name, options)
|
||||
|
||||
# deprecated api
|
||||
deprecated_collection_count_method(association_name)
|
||||
|
@ -834,6 +862,18 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def add_association_callbacks(association_name, options)
|
||||
callbacks = %w(before_add after_add before_remove after_remove)
|
||||
callbacks.each do |callback_name|
|
||||
full_callback_name = "#{callback_name.to_s}_for_#{association_name.to_s}"
|
||||
defined_callbacks = options[callback_name.to_sym]
|
||||
if options.has_key?(callback_name.to_sym)
|
||||
callback_array = defined_callbacks.kind_of?(Array) ? defined_callbacks : [defined_callbacks]
|
||||
class_inheritable_reader full_callback_name.to_sym
|
||||
write_inheritable_array(full_callback_name.to_sym, callback_array)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def extract_record(schema_abbreviations, table_name, row)
|
||||
record = {}
|
||||
|
|
|
@ -21,11 +21,13 @@ module ActiveRecord
|
|||
@owner.transaction do
|
||||
flatten_deeper(records).each do |record|
|
||||
raise_on_type_mismatch(record)
|
||||
callback(:before_add, record)
|
||||
result &&= insert_record(record) unless @owner.new_record?
|
||||
@target << record
|
||||
callback(:after_add, record)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
result and self
|
||||
end
|
||||
|
||||
|
@ -40,8 +42,12 @@ module ActiveRecord
|
|||
return if records.empty?
|
||||
|
||||
@owner.transaction do
|
||||
records.each { |record| callback(:before_remove, record) }
|
||||
delete_records(records)
|
||||
records.each { |record| @target.delete(record) }
|
||||
records.each do |record|
|
||||
@target.delete(record)
|
||||
callback(:after_remove, record)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -113,6 +119,29 @@ module ActiveRecord
|
|||
def flatten_deeper(array)
|
||||
array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
|
||||
end
|
||||
|
||||
def callback(method, record)
|
||||
callbacks_for(method).each do |callback|
|
||||
case callback
|
||||
when Symbol
|
||||
@owner.send(callback, record)
|
||||
when Proc, Method
|
||||
callback.call(@owner, record)
|
||||
else
|
||||
if callback.respond_to?(method)
|
||||
callback.send(method, @owner, record)
|
||||
else
|
||||
raise ActiveRecordError, "Callbacks must be a symbol denoting the method to call, a string to be evaluated, a block to be invoked, or an object responding to the callback method."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def callbacks_for(callback_name)
|
||||
full_callback_name = "#{callback_name.to_s}_for_#{@association_name.to_s}"
|
||||
@owner.class.read_inheritable_attribute(full_callback_name.to_sym) or []
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
39
activerecord/test/fixtures/author.rb
vendored
39
activerecord/test/fixtures/author.rb
vendored
|
@ -1,3 +1,42 @@
|
|||
class Author < ActiveRecord::Base
|
||||
has_many :posts
|
||||
has_many :posts_with_callbacks, :class_name => "Post", :before_add => :log_before_adding,
|
||||
:after_add => :log_after_adding, :before_remove => :log_before_removing,
|
||||
:after_remove => :log_after_removing
|
||||
has_many :posts_with_proc_callbacks, :class_name => "Post",
|
||||
:before_add => Proc.new {|o, r| o.post_log << "before_adding#{r.id}"},
|
||||
:after_add => Proc.new {|o, r| o.post_log << "after_adding#{r.id}"},
|
||||
:before_remove => Proc.new {|o, r| o.post_log << "before_removing#{r.id}"},
|
||||
:after_remove => Proc.new {|o, r| o.post_log << "after_removing#{r.id}"}
|
||||
has_many :posts_with_multiple_callbacks, :class_name => "Post",
|
||||
:before_add => [:log_before_adding, Proc.new {|o, r| o.post_log << "before_adding_proc#{r.id}"}],
|
||||
:after_add => [:log_after_adding, Proc.new {|o, r| o.post_log << "after_adding_proc#{r.id}"}]
|
||||
has_many :unchangable_posts, :class_name => "Post", :before_add => :raise_exception, :after_add => :log_after_adding
|
||||
|
||||
attr_accessor :post_log
|
||||
|
||||
def after_initialize
|
||||
@post_log = []
|
||||
end
|
||||
|
||||
private
|
||||
def log_before_adding(object)
|
||||
@post_log << "before_adding#{object.id}"
|
||||
end
|
||||
|
||||
def log_after_adding(object)
|
||||
@post_log << "after_adding#{object.id}"
|
||||
end
|
||||
|
||||
def log_before_removing(object)
|
||||
@post_log << "before_removing#{object.id}"
|
||||
end
|
||||
|
||||
def log_after_removing(object)
|
||||
@post_log << "after_removing#{object.id}"
|
||||
end
|
||||
|
||||
def raise_exception(object)
|
||||
raise Exception.new("You can't add a post")
|
||||
end
|
||||
end
|
11
activerecord/test/fixtures/project.rb
vendored
11
activerecord/test/fixtures/project.rb
vendored
|
@ -3,6 +3,17 @@ class Project < ActiveRecord::Base
|
|||
has_and_belongs_to_many :developers_named_david, :class_name => "Developer", :conditions => "name = 'David'", :uniq => true
|
||||
has_and_belongs_to_many :salaried_developers, :class_name => "Developer", :conditions => "salary > 0"
|
||||
has_and_belongs_to_many :developers_by_sql, :class_name => "Developer", :delete_sql => "DELETE FROM developers_projects WHERE project_id = \#{id} AND developer_id = \#{record.id}"
|
||||
has_and_belongs_to_many :developers_with_callbacks, :class_name => "Developer", :before_add => Proc.new {|o, r| o.developers_log << "before_adding#{r.id}"},
|
||||
:after_add => Proc.new {|o, r| o.developers_log << "after_adding#{r.id}"},
|
||||
:before_remove => Proc.new {|o, r| o.developers_log << "before_removing#{r.id}"},
|
||||
:after_remove => Proc.new {|o, r| o.developers_log << "after_removing#{r.id}"}
|
||||
|
||||
attr_accessor :developers_log
|
||||
|
||||
def after_initialize
|
||||
@developers_log = []
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
class SpecialProject < Project
|
||||
|
|
Loading…
Reference in a new issue