diff options
Diffstat (limited to 'activerecord/lib')
14 files changed, 209 insertions, 275 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index f5ee4f3ebe..9ba3323bc7 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1094,12 +1094,14 @@ module ActiveRecord # [:primary_key] # Specify the method that returns the primary key used for the association. By default this is +id+. # [:dependent] - # If set to <tt>:destroy</tt> all the associated objects are destroyed - # alongside this object by calling their +destroy+ method. If set to <tt>:delete_all</tt> all associated - # objects are deleted *without* calling their +destroy+ method. If set to <tt>:nullify</tt> all associated - # objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. If set to - # <tt>:restrict</tt> an error will be added to the object, preventing its deletion, if any associated - # objects are present. + # Controls what happens to the associated objects when + # their owner is destroyed: + # + # * <tt>:destroy</tt> causes all the associated objects to also be destroyed + # * <tt>:delete_all</tt> causes all the asssociated objects to be deleted directly from the database (so callbacks will not execute) + # * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Callbacks are not executed. + # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records + # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects # # If using with the <tt>:through</tt> option, the association on the join model must be # a +belongs_to+, and the records which get deleted are the join records, rather than @@ -1203,11 +1205,14 @@ module ActiveRecord # from the association name. So <tt>has_one :manager</tt> will by default be linked to the Manager class, but # if the real class name is Person, you'll have to specify it with this option. # [:dependent] - # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to - # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. - # If set to <tt>:nullify</tt>, the associated object's foreign key is set to +NULL+. - # If set to <tt>:restrict</tt>, an error will be added to the object, preventing its deletion, if an - # associated object is present. + # Controls what happens to the associated object when + # its owner is destroyed: + # + # * <tt>:destroy</tt> causes the associated object to also be destroyed + # * <tt>:delete</tt> causes the asssociated object to be deleted directly from the database (so callbacks will not execute) + # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed. + # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record + # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index ddfc6f6c05..75f72c1a46 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -2,6 +2,11 @@ module ActiveRecord # = Active Record Belongs To Associations module Associations class BelongsToAssociation < SingularAssociation #:nodoc: + + def handle_dependency + target.send(options[:dependent]) if load_target + end + def replace(record) raise_on_type_mismatch(record) if record diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index c3f32b5ed9..1df876bf62 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -39,6 +39,7 @@ module ActiveRecord::Associations::Builder def build validate_options define_accessors + configure_dependency if options[:dependent] @reflection = model.create_reflection(macro, name, scope, options, model) super # provides an extension point @reflection @@ -52,70 +53,54 @@ module ActiveRecord::Associations::Builder Association.valid_options end - private - - def validate_options - options.assert_valid_keys(valid_options) - end + def validate_options + options.assert_valid_keys(valid_options) + end - def define_accessors - define_readers - define_writers - end + def define_accessors + define_readers + define_writers + end - def define_readers - name = self.name - mixin.redefine_method(name) do |*params| - association(name).reader(*params) + def define_readers + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}(*args) + association(:#{name}).reader(*args) end - end + CODE + end - def define_writers - name = self.name - mixin.redefine_method("#{name}=") do |value| - association(name).writer(value) + def define_writers + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}=(value) + association(:#{name}).writer(value) end - end - - def check_valid_dependent!(dependent, valid_options) - unless valid_options.include?(dependent) - valid_options_message = valid_options.map(&:inspect).to_sentence( - words_connector: ', ', two_words_connector: ' or ', last_word_connector: ' or ') + CODE + end - raise ArgumentError, "The :dependent option expects either " \ - "#{valid_options_message} (#{dependent.inspect})" - end + def configure_dependency + unless valid_dependent_options.include? options[:dependent] + raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{options[:dependent]}" end - def dependent_restrict_raises? - ActiveRecord::Base.dependent_restrict_raises == true + if options[:dependent] == :restrict + ActiveSupport::Deprecation.warn( + "The :restrict option is deprecated. Please use :restrict_with_exception instead, which " \ + "provides the same functionality." + ) end - def dependent_restrict_deprecation_warning - if dependent_restrict_raises? - msg = "In the next release, `:dependent => :restrict` will not raise a `DeleteRestrictionError`. "\ - "Instead, it will add an error on the model. To fix this warning, make sure your code " \ - "isn't relying on a `DeleteRestrictionError` and then add " \ - "`config.active_record.dependent_restrict_raises = false` to your application config." - ActiveSupport::Deprecation.warn msg + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{macro}_dependent_for_#{name} + association(:#{name}).handle_dependency end - end + CODE - def define_restrict_dependency_method - name = self.name - mixin.redefine_method(dependency_method_name) do - has_one_macro = association(name).reflection.macro == :has_one - if has_one_macro ? !send(name).nil? : send(name).exists? - if dependent_restrict_raises? - raise ActiveRecord::DeleteRestrictionError.new(name) - else - key = has_one_macro ? "one" : "many" - errors.add(:base, :"restrict_dependent_destroy.#{key}", - :record => self.class.human_attribute_name(name).downcase) - return false - end - end - end - end + model.before_destroy "#{macro}_dependent_for_#{name}" + end + + def valid_dependent_options + raise NotImplementedError + end end end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index f205a456f7..2f2600b7fb 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -1,4 +1,3 @@ - module ActiveRecord::Associations::Builder class BelongsTo < SingularAssociation #:nodoc: def macro @@ -17,72 +16,51 @@ module ActiveRecord::Associations::Builder reflection = super add_counter_cache_callbacks(reflection) if options[:counter_cache] add_touch_callbacks(reflection) if options[:touch] - configure_dependency reflection end - private - - def add_counter_cache_callbacks(reflection) - cache_column = reflection.counter_cache_column - name = self.name + def add_counter_cache_callbacks(reflection) + cache_column = reflection.counter_cache_column - method_name = "belongs_to_counter_cache_after_create_for_#{name}" - mixin.redefine_method(method_name) do - record = send(name) - record.class.increment_counter(cache_column, record.id) unless record.nil? + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def belongs_to_counter_cache_after_create_for_#{name} + record = #{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}" - mixin.redefine_method(method_name) do + def belongs_to_counter_cache_before_destroy_for_#{name} unless marked_for_destruction? - record = send(name) - record.class.decrement_counter(cache_column, record.id) unless record.nil? + record = #{name} + record.class.decrement_counter(:#{cache_column}, record.id) unless record.nil? end end - model.before_destroy(method_name) + CODE - model.send(:module_eval, - "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)", __FILE__, __LINE__ - ) - end + model.after_create "belongs_to_counter_cache_after_create_for_#{name}" + model.before_destroy "belongs_to_counter_cache_before_destroy_for_#{name}" - def add_touch_callbacks(reflection) - name = self.name - method_name = "belongs_to_touch_after_save_or_destroy_for_#{name}" - touch = options[:touch] + klass = reflection.class_name.safe_constantize + klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly) + end - mixin.redefine_method(method_name) do - record = send(name) + def add_touch_callbacks(reflection) + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def belongs_to_touch_after_save_or_destroy_for_#{name} + record = #{name} unless record.nil? - if touch == true - record.touch - else - record.touch(touch) - end + record.touch #{options[:touch].inspect if options[:touch] != true} end end + CODE - model.after_save(method_name) - model.after_touch(method_name) - model.after_destroy(method_name) - end - - def configure_dependency - if dependent = options[:dependent] - check_valid_dependent! dependent, [:destroy, :delete] + model.after_save "belongs_to_touch_after_save_or_destroy_for_#{name}" + model.after_touch "belongs_to_touch_after_save_or_destroy_for_#{name}" + model.after_destroy "belongs_to_touch_after_save_or_destroy_for_#{name}" + end - method_name = "belongs_to_dependent_#{dependent}_for_#{name}" - model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1) - def #{method_name} - association = #{name} - association.#{dependent} if association - end - eoruby - model.after_destroy method_name - end - end + def valid_dependent_options + [:destroy, :delete] + end end end diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index 3fb0a57450..1b382f7285 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -1,4 +1,3 @@ - module ActiveRecord::Associations::Builder class CollectionAssociation < Association #:nodoc: CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] @@ -34,53 +33,53 @@ module ActiveRecord::Associations::Builder end end - private - - def wrap_block_extension - if block_extension - @extension_module = mod = Module.new(&block_extension) - silence_warnings do - model.parent.const_set(extension_module_name, mod) - end + def wrap_block_extension + if block_extension + @extension_module = mod = Module.new(&block_extension) + silence_warnings do + model.parent.const_set(extension_module_name, mod) + end - prev_scope = @scope + prev_scope = @scope - if prev_scope - @scope = proc { |owner| instance_exec(owner, &prev_scope).extending(mod) } - else - @scope = proc { extending(mod) } - end + if prev_scope + @scope = proc { |owner| instance_exec(owner, &prev_scope).extending(mod) } + else + @scope = proc { extending(mod) } end end + end - def extension_module_name - @extension_module_name ||= "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension" - end + def extension_module_name + @extension_module_name ||= "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension" + end - def define_callback(callback_name) - full_callback_name = "#{callback_name}_for_#{name}" + def define_callback(callback_name) + full_callback_name = "#{callback_name}_for_#{name}" - # TODO : why do i need method_defined? I think its because of the inheritance chain - model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name) - model.send("#{full_callback_name}=", Array(options[callback_name.to_sym])) - end + # TODO : why do i need method_defined? I think its because of the inheritance chain + model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name) + model.send("#{full_callback_name}=", Array(options[callback_name.to_sym])) + end - def define_readers - super + def define_readers + super - name = self.name - mixin.redefine_method("#{name.to_s.singularize}_ids") do - association(name).ids_reader + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name.to_s.singularize}_ids + association(:#{name}).ids_reader end - end + CODE + end - def define_writers - super + def define_writers + super - name = self.name - mixin.redefine_method("#{name.to_s.singularize}_ids=") do |ids| - association(name).ids_writer(ids) + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name.to_s.singularize}_ids=(ids) + association(:#{name}).ids_writer(ids) end - end + CODE + end end end diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 8df28ad876..bdac02b5bf 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -24,18 +24,16 @@ module ActiveRecord::Associations::Builder end end - private - - 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 - }) - end + def define_destroy_hook + name = self.name + model.send(:include, Module.new { + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def destroy_associations + association(:#{name}).delete_all + super + end + RUBY + }) + end end end diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 9e60dbc30b..ab8225460a 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -1,4 +1,3 @@ - module ActiveRecord::Associations::Builder class HasMany < CollectionAssociation #:nodoc: def macro @@ -9,52 +8,8 @@ module ActiveRecord::Associations::Builder super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of] end - def build - reflection = super - configure_dependency - reflection + def valid_dependent_options + [:destroy, :delete_all, :nullify, :restrict, :restrict_with_error, :restrict_with_exception] end - - private - - def configure_dependency - if dependent = options[:dependent] - check_valid_dependent! dependent, [:destroy, :delete_all, :nullify, :restrict] - dependent_restrict_deprecation_warning if dependent == :restrict - - send("define_#{dependent}_dependency_method") - model.before_destroy dependency_method_name - end - end - - def define_destroy_dependency_method - name = self.name - 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 - o.mark_for_destruction - end - - send(name).delete_all - end - end - - def define_delete_all_dependency_method - name = self.name - mixin.redefine_method(dependency_method_name) do - association(name).delete_all - end - end - - def define_nullify_dependency_method - name = self.name - mixin.redefine_method(dependency_method_name) do - send(name).delete_all - end - end - - def dependency_method_name - "has_many_dependent_for_#{name}" - end end end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index 9c84f1913a..0da564f402 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -1,4 +1,3 @@ - module ActiveRecord::Associations::Builder class HasOne < SingularAssociation #:nodoc: def macro @@ -15,35 +14,12 @@ module ActiveRecord::Associations::Builder !options[:through] end - def build - reflection = super - configure_dependency unless options[:through] - reflection + def configure_dependency + super unless options[:through] end - private - - def configure_dependency - if dependent = options[:dependent] - check_valid_dependent! dependent, [:destroy, :delete, :nullify, :restrict] - dependent_restrict_deprecation_warning if dependent == :restrict - - send("define_#{dependent}_dependency_method") - model.before_destroy dependency_method_name - end - end - - def define_destroy_dependency_method - 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 dependency_method_name - "has_one_dependent_#{options[:dependent]}_for_#{name}" - end + def valid_dependent_options + [:destroy, :delete, :nullify, :restrict, :restrict_with_error, :restrict_with_exception] + end end end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 90a4b7c2ef..6a5830e57f 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -13,22 +13,20 @@ module ActiveRecord::Associations::Builder define_constructors if constructable? end - private - - def define_constructors - name = self.name - - mixin.redefine_method("build_#{name}") do |*params, &block| - association(name).build(*params, &block) + def define_constructors + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def build_#{name}(*args, &block) + association(:#{name}).build(*args, &block) end - mixin.redefine_method("create_#{name}") do |*params, &block| - association(name).create(*params, &block) + def create_#{name}(*args, &block) + association(:#{name}).create(*args, &block) end - mixin.redefine_method("create_#{name}!") do |*params, &block| - association(name).create!(*params, &block) + def create_#{name}!(*args, &block) + association(:#{name}).create!(*args, &block) end - end + CODE + end end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 7a363c896e..74864d271f 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -7,6 +7,28 @@ module ActiveRecord # is provided by its child HasManyThroughAssociation. class HasManyAssociation < CollectionAssociation #:nodoc: + def handle_dependency + case options[:dependent] + when :restrict, :restrict_with_exception + raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty? + + when :restrict_with_error + unless empty? + record = klass.human_attribute_name(reflection.name).downcase + owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record) + false + end + + else + if options[:dependent] == :destroy + # No point in executing the counter update since we're going to destroy the parent anyway + load_target.each(&:mark_for_destruction) + end + + delete_all + end + end + def insert_record(record, validate = true, raise = false) set_owner_attributes(record) diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 191c4e8e39..dd7da59a86 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -3,23 +3,43 @@ module ActiveRecord # = Active Record Belongs To Has One Association module Associations class HasOneAssociation < SingularAssociation #:nodoc: - def replace(record, save = true) - raise_on_type_mismatch(record) if record - load_target - reflection.klass.transaction do - if target && target != record - remove_target!(options[:dependent]) unless target.destroyed? + def handle_dependency + case options[:dependent] + when :restrict, :restrict_with_exception + raise ActiveRecord::DeleteRestrictionError.new(reflection.name) if load_target + + when :restrict_with_error + if load_target + record = klass.human_attribute_name(reflection.name).downcase + owner.errors.add(:base, :"restrict_dependent_destroy.one", record: record) + false end - if record - set_owner_attributes(record) - set_inverse_instance(record) + else + delete + end + end - if owner.persisted? && save && !record.save - nullify_owner_attributes(record) - set_owner_attributes(target) if target - raise RecordNotSaved, "Failed to save the new associated #{reflection.name}." + def replace(record, save = true) + raise_on_type_mismatch(record) if record + load_target + + # If target and record are nil, or target is equal to record, + # we don't need to have transaction. + if (target || record) && target != record + reflection.klass.transaction do + remove_target!(options[:dependent]) if target && !target.destroyed? + + if record + set_owner_attributes(record) + set_inverse_instance(record) + + if owner.persisted? && save && !record.save + nullify_owner_attributes(record) + set_owner_attributes(target) if target + raise RecordNotSaved, "Failed to save the new associated #{reflection.name}." + end end end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 1145d2138c..0fddfdf0cb 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -82,15 +82,6 @@ module ActiveRecord # The connection handler config_attribute :connection_handler - ## - # :singleton-method: - # Specifies whether or not has_many or has_one association option - # :dependent => :restrict raises an exception. If set to true, the - # ActiveRecord::DeleteRestrictionError exception will be raised - # along with a DEPRECATION WARNING. If set to false, an error would - # be added to the model instead. - config_attribute :dependent_restrict_raises - %w(logger configurations default_timezone schema_format timestamped_migrations).each do |name| config_attribute name, global: true end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 9ed3256ae9..1abbc58314 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -258,6 +258,8 @@ module ActiveRecord # # Update all books that match conditions, but limit it to 5 ordered by date # Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(:author => 'David') def update_all(updates) + raise ArgumentError, "Empty list of attributes to change" if updates.blank? + stmt = Arel::UpdateManager.new(arel.engine) stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) @@ -466,7 +468,7 @@ module ActiveRecord # Returns sql statement for the relation. # # Users.where(name: 'Oscar').to_sql - # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar' + # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar' def to_sql @to_sql ||= klass.connection.to_sql(arel, bind_values.dup) end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 9cb9b4627b..5c3399e2aa 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -327,7 +327,7 @@ module ActiveRecord def restore_transaction_record_state(force = false) #:nodoc: if defined?(@_start_transaction_state) @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - if @_start_transaction_state[:level] < 1 + if @_start_transaction_state[:level] < 1 || force restore_state = remove_instance_variable(:@_start_transaction_state) was_frozen = @attributes.frozen? @attributes = @attributes.dup if was_frozen |