From 63ff495bdf90e0ab20114a49db5cffe3cb9ef2fd Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono Date: Sat, 19 May 2018 11:46:03 +0900 Subject: Fix dirty tracking after rollback. Currently the rollback only restores primary key value, `new_record?`, `destroyed?`, and `frozen?`. Since the `save` clears current dirty attribute states, retrying save after rollback will causes no change saved if partial writes is enabled (by default). This makes `remember_transaction_record_state` remembers original values then restores dirty attribute states after rollback. Fixes #15018. Fixes #30167. Fixes #33868. Fixes #33443. Closes #33444. Closes #34504. --- .../lib/active_record/attribute_methods/before_type_cast.rb | 3 +++ activerecord/lib/active_record/attribute_methods/dirty.rb | 10 ++++++++++ .../lib/active_record/attribute_methods/primary_key.rb | 6 ------ activerecord/lib/active_record/attribute_methods/read.rb | 5 +---- activerecord/lib/active_record/attribute_methods/write.rb | 9 +++------ 5 files changed, 17 insertions(+), 16 deletions(-) (limited to 'activerecord/lib/active_record/attribute_methods') diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index dc239ff9ea..affcf2a4db 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -46,6 +46,7 @@ module ActiveRecord # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21" # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21" def read_attribute_before_type_cast(attr_name) + sync_with_transaction_state @attributes[attr_name.to_s].value_before_type_cast end @@ -60,6 +61,7 @@ module ActiveRecord # task.attributes_before_type_cast # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil} def attributes_before_type_cast + sync_with_transaction_state @attributes.values_before_type_cast end @@ -71,6 +73,7 @@ module ActiveRecord end def attribute_came_from_user?(attribute_name) + sync_with_transaction_state @attributes[attribute_name].came_from_user? end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 68ac8475b0..942fe48635 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -156,6 +156,16 @@ module ActiveRecord end private + def mutations_from_database + sync_with_transaction_state + super + end + + def mutations_before_last_save + sync_with_transaction_state + super + end + def write_attribute_without_type_cast(attr_name, value) name = attr_name.to_s if self.class.attribute_alias?(name) diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 6af5346fa7..feaef72a30 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -16,39 +16,33 @@ module ActiveRecord # Returns the primary key column's value. def id - sync_with_transaction_state primary_key = self.class.primary_key _read_attribute(primary_key) if primary_key end # Sets the primary key column's value. def id=(value) - sync_with_transaction_state primary_key = self.class.primary_key _write_attribute(primary_key, value) if primary_key end # Queries the primary key column's value. def id? - sync_with_transaction_state query_attribute(self.class.primary_key) end # Returns the primary key column's value before type cast. def id_before_type_cast - sync_with_transaction_state read_attribute_before_type_cast(self.class.primary_key) end # Returns the primary key column's previous value. def id_was - sync_with_transaction_state attribute_was(self.class.primary_key) end # Returns the primary key column's value from the database. def id_in_database - sync_with_transaction_state attribute_in_database(self.class.primary_key) end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index ffac5313ad..84b1ec2fea 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -9,14 +9,11 @@ module ActiveRecord private def define_method_attribute(name) - sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key - ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method( generated_attribute_methods, name ) do |temp_method_name, attr_name_expr| generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{temp_method_name} - #{sync_with_transaction_state} name = #{attr_name_expr} _read_attribute(name) { |n| missing_attribute(n, caller) } end @@ -36,13 +33,13 @@ module ActiveRecord primary_key = self.class.primary_key name = primary_key if name == "id" && primary_key - sync_with_transaction_state if name == primary_key _read_attribute(name, &block) end # This method exists to avoid the expensive primary_key check internally, without # breaking compatibility with the read_attribute API def _read_attribute(attr_name, &block) # :nodoc + sync_with_transaction_state @attributes.fetch_value(attr_name.to_s, &block) end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index d5ba2f42cb..d1cfe43bb2 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -13,15 +13,12 @@ module ActiveRecord private def define_method_attribute=(name) - sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key - ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method( generated_attribute_methods, name, writer: true, ) do |temp_method_name, attr_name_expr| generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{temp_method_name}(value) name = #{attr_name_expr} - #{sync_with_transaction_state} _write_attribute(name, value) end RUBY @@ -40,21 +37,21 @@ module ActiveRecord primary_key = self.class.primary_key name = primary_key if name == "id" && primary_key - sync_with_transaction_state if name == primary_key _write_attribute(name, value) end # This method exists to avoid the expensive primary_key check internally, without # breaking compatibility with the write_attribute API def _write_attribute(attr_name, value) # :nodoc: + sync_with_transaction_state @attributes.write_from_user(attr_name.to_s, value) value end private def write_attribute_without_type_cast(attr_name, value) - name = attr_name.to_s - @attributes.write_cast_value(name, value) + sync_with_transaction_state + @attributes.write_cast_value(attr_name.to_s, value) value end -- cgit v1.2.3