diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 50166c43857eec2a3bf57f38a9af3d667af7a2fc..8d093c81def78bbb190295df3cd71d5a2ca63b05 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -102,12 +102,19 @@ def becomes(klass) became end - # Updates a single attribute and saves the record without going through the normal validation procedure. - # This is especially useful for boolean flags on existing records. The regular +update_attribute+ method - # in Base is replaced with this when the validations module is mixed in, which it is by default. + # Updates a single attribute and saves the record without going through the normal validation procedure + # or callbacks. This is especially useful for boolean flags on existing records. def update_attribute(name, value) send("#{name}=", value) - save(:validate => false) + primary_key = self.class.primary_key + h = {name => value} + if should_record_update_timestamps + self.send(:record_update_timestamps) + current_time = current_time_from_proper_timezone + timestamp_attributes_for_update_in_model.each { |column| h.merge!(column => current_time) } + end + self.class.update_all(h, {primary_key => self[primary_key]}) == 1 + @changed_attributes.delete(name.to_s) end # Updates all the attributes from the passed-in Hash and saves the record. @@ -234,4 +241,4 @@ def attributes_from_column_definition end end end -end \ No newline at end of file +end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index da8324ddcc87713fcc984ec3c5848a53e4f0ad5a..e6d52744df14c1467d29ace02654f2c7f953abbc 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -58,14 +58,22 @@ def create #:nodoc: end def update(*args) #:nodoc: - if record_timestamps && (!partial_updates? || changed?) + record_update_timestamps + super + end + + def record_update_timestamps + if should_record_update_timestamps current_time = current_time_from_proper_timezone timestamp_attributes_for_update_in_model.each { |column| write_attribute(column.to_s, current_time) } end + end - super + def should_record_update_timestamps + record_timestamps && (!partial_updates? || changed?) end + def timestamp_attributes_for_update #:nodoc: [:updated_at, :updated_on] end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 329ca38f99c6741cce86a74305a9f76d1b7046bd..09bdd13cd4fcc41c2a3cc8a8ab027a22ab721427 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -893,6 +893,46 @@ def test_update_attribute assert !Topic.find(1).approved? end + def test_update_attribute_with_one_changed_and_one_updated + t = Topic.order('id').limit(1).first + title, author_name = t.title, t.author_name + t.author_name = 'John' + t.update_attribute(:title, 'super_title') + assert_equal 'John', t.author_name + assert_equal 'super_title', t.title + assert t.changed?, "topic should have changed" + assert t.author_name_changed?, "author_name should have changed" + assert !t.title_changed?, "title should not have changed" + assert_nil t.title_change, 'title change should be nil' + assert_equal ['author_name'], t.changed + + t.reload + assert_equal 'David', t.author_name + assert_equal 'super_title', t.title + end + + def test_update_attribute_with_one_updated + t = Topic.first + title = t.title + t.update_attribute(:title, 'super_title') + assert_equal 'super_title', t.title + assert !t.changed?, "topic should not have changed" + assert !t.title_changed?, "title should not have changed" + assert_nil t.title_change, 'title change should be nil' + + t.reload + assert_equal 'super_title', t.title + end + + def test_update_attribute_for_udpated_at_on + developer = Developer.find(1) + updated_at = developer.updated_at + developer.update_attribute(:salary, 80001) + assert_not_equal updated_at, developer.updated_at + developer.reload + assert_not_equal updated_at, developer.updated_at + end + def test_update_attributes topic = Topic.find(1) assert !topic.approved? diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 75f7453aa99ac36758a0d695df4265ba0db359ab..837386ed24ac5b9d0ce284bcc21474ff8f232d12 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -475,10 +475,9 @@ def test_previous_changes pirate = Pirate.find_by_catchphrase("Ahoy!") pirate.update_attribute(:catchphrase, "Ninjas suck!") - assert_equal 2, pirate.previous_changes.size - assert_equal ["Ahoy!", "Ninjas suck!"], pirate.previous_changes['catchphrase'] - assert_not_nil pirate.previous_changes['updated_on'][0] - assert_not_nil pirate.previous_changes['updated_on'][1] + assert_equal 0, pirate.previous_changes.size + assert_nil pirate.previous_changes['catchphrase'] + assert_nil pirate.previous_changes['updated_on'] assert !pirate.previous_changes.key?('parrot_id') assert !pirate.previous_changes.key?('created_on') end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 62237f955b5260b4008b4796aa20a53fb8f12123..c9ea0d8c403da6abfad42e4c12dec09c8d17189b 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -195,7 +195,7 @@ def test_should_destroy_an_existing_record_if_there_is_a_matching_id_and_destroy [1, '1', true, 'true'].each do |truth| @pirate.reload.create_ship(:name => 'Mister Pablo') assert_difference('Ship.count', -1) do - @pirate.update_attribute(:ship_attributes, { :id => @pirate.ship.id, :_destroy => truth }) + @pirate.update_attributes(:ship_attributes => { :id => @pirate.ship.id, :_destroy => truth }) end end end @@ -203,7 +203,7 @@ def test_should_destroy_an_existing_record_if_there_is_a_matching_id_and_destroy def test_should_not_destroy_an_existing_record_if_destroy_is_not_truthy [nil, '0', 0, 'false', false].each do |not_truth| assert_no_difference('Ship.count') do - @pirate.update_attribute(:ship_attributes, { :id => @pirate.ship.id, :_destroy => not_truth }) + @pirate.update_attributes(:ship_attributes => { :id => @pirate.ship.id, :_destroy => not_truth }) end end end @@ -212,7 +212,7 @@ def test_should_not_destroy_an_existing_record_if_allow_destroy_is_false Pirate.accepts_nested_attributes_for :ship, :allow_destroy => false, :reject_if => proc { |attributes| attributes.empty? } assert_no_difference('Ship.count') do - @pirate.update_attribute(:ship_attributes, { :id => @pirate.ship.id, :_destroy => '1' }) + @pirate.update_attributes(:ship_attributes => { :id => @pirate.ship.id, :_destroy => '1' }) end Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } @@ -247,13 +247,13 @@ def test_should_automatically_enable_autosave_on_the_association end def test_should_accept_update_only_option - @pirate.update_attribute(:update_only_ship_attributes, { :id => @pirate.ship.id, :name => 'Mayflower' }) + @pirate.update_attributes(:update_only_ship_attributes => { :id => @pirate.ship.id, :name => 'Mayflower' }) end def test_should_create_new_model_when_nothing_is_there_and_update_only_is_true @ship.delete assert_difference('Ship.count', 1) do - @pirate.reload.update_attribute(:update_only_ship_attributes, { :name => 'Mayflower' }) + @pirate.reload.update_attributes(:update_only_ship_attributes => { :name => 'Mayflower' }) end end @@ -353,7 +353,7 @@ def test_should_destroy_an_existing_record_if_there_is_a_matching_id_and_destroy [1, '1', true, 'true'].each do |truth| @ship.reload.create_pirate(:catchphrase => 'Arr') assert_difference('Pirate.count', -1) do - @ship.update_attribute(:pirate_attributes, { :id => @ship.pirate.id, :_destroy => truth }) + @ship.update_attributes(:pirate_attributes => { :id => @ship.pirate.id, :_destroy => truth }) end end end @@ -361,7 +361,7 @@ def test_should_destroy_an_existing_record_if_there_is_a_matching_id_and_destroy def test_should_not_destroy_an_existing_record_if_destroy_is_not_truthy [nil, '0', 0, 'false', false].each do |not_truth| assert_no_difference('Pirate.count') do - @ship.update_attribute(:pirate_attributes, { :id => @ship.pirate.id, :_destroy => not_truth }) + @ship.update_attributes(:pirate_attributes => { :id => @ship.pirate.id, :_destroy => not_truth }) end end end @@ -370,7 +370,7 @@ def test_should_not_destroy_an_existing_record_if_allow_destroy_is_false Ship.accepts_nested_attributes_for :pirate, :allow_destroy => false, :reject_if => proc { |attributes| attributes.empty? } assert_no_difference('Pirate.count') do - @ship.update_attribute(:pirate_attributes, { :id => @ship.pirate.id, :_destroy => '1' }) + @ship.update_attributes(:pirate_attributes => { :id => @ship.pirate.id, :_destroy => '1' }) end Ship.accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } @@ -398,7 +398,7 @@ def test_should_automatically_enable_autosave_on_the_association def test_should_create_new_model_when_nothing_is_there_and_update_only_is_true @pirate.delete assert_difference('Pirate.count', 1) do - @ship.reload.update_attribute(:update_only_pirate_attributes, { :catchphrase => 'Arr' }) + @ship.reload.update_attributes(:update_only_pirate_attributes => { :catchphrase => 'Arr' }) end end