Skip to content

Commit 0638d35

Browse files
authored
Merge pull request rails#44141 from drewtempelmeyer/activerecord-update-attributes-exclamation
Add ActiveRecord::Persistence#update_attribute!
2 parents bfb756b + c03fddf commit 0638d35

File tree

3 files changed

+109
-1
lines changed

3 files changed

+109
-1
lines changed

activerecord/CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,26 @@
1+
* Add `update_attribute!` to `ActiveRecord::Persistence`
2+
3+
Similar to `update_attribute`, but raises `ActiveRecord::RecordNotSaved` when a `before_*` callback throws `:abort`.
4+
5+
```ruby
6+
class Topic < ActiveRecord::Base
7+
before_save :check_title
8+
9+
def check_title
10+
throw(:abort) if title == "abort"
11+
end
12+
end
13+
14+
topic = Topic.create(title: "Test Title")
15+
# #=> #<Topic title: "Test Title">
16+
topic.update_attribute!(:title, "Another Title")
17+
# #=> #<Topic title: "Another Title">
18+
topic.update_attribute!(:title, "abort")
19+
# raises ActiveRecord::RecordNotSaved
20+
```
21+
22+
*Drew Tempelmeyer*
23+
124
* Avoid loading every record in `ActiveRecord::Relation#pretty_print`
225

326
```ruby

activerecord/lib/active_record/persistence.rb

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -747,7 +747,7 @@ def becomes!(klass)
747747
# * updated_at/updated_on column is updated if that column is available.
748748
# * Updates all the attributes that are dirty in this object.
749749
#
750-
# This method raises an ActiveRecord::ActiveRecordError if the
750+
# This method raises an ActiveRecord::ActiveRecordError if the
751751
# attribute is marked as readonly.
752752
#
753753
# Also see #update_column.
@@ -759,6 +759,28 @@ def update_attribute(name, value)
759759
save(validate: false)
760760
end
761761

762+
# Updates a single attribute and saves the record.
763+
# This is especially useful for boolean flags on existing records. Also note that
764+
#
765+
# * Validation is skipped.
766+
# * \Callbacks are invoked.
767+
# * updated_at/updated_on column is updated if that column is available.
768+
# * Updates all the attributes that are dirty in this object.
769+
#
770+
# This method raises an ActiveRecord::ActiveRecordError if the
771+
# attribute is marked as readonly.
772+
#
773+
# If any of the <tt>before_*</tt> callbacks throws +:abort+ the action is cancelled
774+
# and #update_attribute! raises ActiveRecord::RecordNotSaved. See
775+
# ActiveRecord::Callbacks for further details.
776+
def update_attribute!(name, value)
777+
name = name.to_s
778+
verify_readonly_attribute(name)
779+
public_send("#{name}=", value)
780+
781+
save!(validate: false)
782+
end
783+
762784
# Updates the attributes of the model from the passed-in hash and saves the
763785
# record, all wrapped in a transaction. If the object is invalid, the saving
764786
# will fail and false will be returned.

activerecord/test/cases/persistence_test.rb

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,69 @@ def test_update_attribute_for_updated_at_on
813813
assert_not_equal prev_month, developer.updated_at
814814
end
815815

816+
def test_update_attribute!
817+
assert_not_predicate Topic.find(1), :approved?
818+
Topic.find(1).update_attribute!("approved", true)
819+
assert_predicate Topic.find(1), :approved?
820+
821+
Topic.find(1).update_attribute!(:approved, false)
822+
assert_not_predicate Topic.find(1), :approved?
823+
824+
Topic.find(1).update_attribute!(:change_approved_before_save, true)
825+
assert_predicate Topic.find(1), :approved?
826+
end
827+
828+
def test_update_attribute_for_readonly_attribute!
829+
minivan = Minivan.find("m1")
830+
assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_attribute!(:color, "black") }
831+
end
832+
833+
def test_update_attribute_with_one_updated!
834+
t = Topic.first
835+
t.update_attribute!(:title, "super_title")
836+
assert_equal "super_title", t.title
837+
assert_not t.changed?, "topic should not have changed"
838+
assert_not t.title_changed?, "title should not have changed"
839+
assert_nil t.title_change, "title change should be nil"
840+
841+
t.reload
842+
assert_equal "super_title", t.title
843+
end
844+
845+
def test_update_attribute_for_updated_at_on!
846+
developer = Developer.find(1)
847+
prev_month = Time.now.prev_month.change(usec: 0)
848+
849+
developer.update_attribute!(:updated_at, prev_month)
850+
assert_equal prev_month, developer.updated_at
851+
852+
developer.update_attribute!(:salary, 80001)
853+
assert_not_equal prev_month, developer.updated_at
854+
855+
developer.reload
856+
assert_not_equal prev_month, developer.updated_at
857+
end
858+
859+
def test_update_attribute_for_aborted_callback!
860+
klass = Class.new(Topic) do
861+
def self.name; "Topic"; end
862+
863+
before_update :throw_abort
864+
865+
def throw_abort
866+
throw(:abort)
867+
end
868+
end
869+
870+
t = klass.create(title: "New Topic", author_name: "Not David")
871+
872+
assert_raises(ActiveRecord::RecordNotSaved) { t.update_attribute!(:title, "super_title") }
873+
874+
t_reloaded = Topic.find(t.id)
875+
876+
assert_equal "New Topic", t_reloaded.title
877+
end
878+
816879
def test_update_column
817880
topic = Topic.find(1)
818881
topic.update_column("approved", true)

0 commit comments

Comments
 (0)