diff options
author | David Heinemeier Hansson <david@loudthinking.com> | 2005-07-04 08:43:57 +0000 |
---|---|---|
committer | David Heinemeier Hansson <david@loudthinking.com> | 2005-07-04 08:43:57 +0000 |
commit | 4180e57b7091f529a671199c22530058d5bb4cf7 (patch) | |
tree | e37efc25b556909641fdd715a023a7ed4317623f | |
parent | 555072e8e9c4df577fbd10e8a34fa4036e31649c (diff) | |
download | rails-4180e57b7091f529a671199c22530058d5bb4cf7.tar.gz rails-4180e57b7091f529a671199c22530058d5bb4cf7.tar.bz2 rails-4180e57b7091f529a671199c22530058d5bb4cf7.zip |
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
-rw-r--r-- | activerecord/CHANGELOG | 13 | ||||
-rwxr-xr-x | activerecord/lib/active_record/associations.rb | 46 | ||||
-rw-r--r-- | activerecord/lib/active_record/associations/association_collection.rb | 33 | ||||
-rw-r--r-- | activerecord/test/fixtures/author.rb | 39 | ||||
-rw-r--r-- | activerecord/test/fixtures/project.rb | 11 |
5 files changed, 137 insertions, 5 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 1f03b2a427..1b631b2bd9 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -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] diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 05d8bd9ad0..42e5a9f1c7 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -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 = {} diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 4495d92151..140a12eacf 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -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 diff --git a/activerecord/test/fixtures/author.rb b/activerecord/test/fixtures/author.rb index e159d38597..ea0094de39 100644 --- a/activerecord/test/fixtures/author.rb +++ b/activerecord/test/fixtures/author.rb @@ -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
\ No newline at end of file diff --git a/activerecord/test/fixtures/project.rb b/activerecord/test/fixtures/project.rb index a40aca95e4..9a13c9eead 100644 --- a/activerecord/test/fixtures/project.rb +++ b/activerecord/test/fixtures/project.rb @@ -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 |