Skip to content

Commit

Permalink
Merge pull request rails#44141 from drewtempelmeyer/activerecord-upda…
Browse files Browse the repository at this point in the history
…te-attributes-exclamation

Add ActiveRecord::Persistence#update_attribute!
  • Loading branch information
rafaelfranca authored Jan 11, 2022
2 parents bfb756b + c03fddf commit 0638d35
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 1 deletion.
23 changes: 23 additions & 0 deletions activerecord/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
* Add `update_attribute!` to `ActiveRecord::Persistence`

Similar to `update_attribute`, but raises `ActiveRecord::RecordNotSaved` when a `before_*` callback throws `:abort`.

```ruby
class Topic < ActiveRecord::Base
before_save :check_title

def check_title
throw(:abort) if title == "abort"
end
end

topic = Topic.create(title: "Test Title")
# #=> #<Topic title: "Test Title">
topic.update_attribute!(:title, "Another Title")
# #=> #<Topic title: "Another Title">
topic.update_attribute!(:title, "abort")
# raises ActiveRecord::RecordNotSaved
```

*Drew Tempelmeyer*

* Avoid loading every record in `ActiveRecord::Relation#pretty_print`

```ruby
Expand Down
24 changes: 23 additions & 1 deletion activerecord/lib/active_record/persistence.rb
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,7 @@ def becomes!(klass)
# * updated_at/updated_on column is updated if that column is available.
# * Updates all the attributes that are dirty in this object.
#
# This method raises an ActiveRecord::ActiveRecordError if the
# This method raises an ActiveRecord::ActiveRecordError if the
# attribute is marked as readonly.
#
# Also see #update_column.
Expand All @@ -759,6 +759,28 @@ def update_attribute(name, value)
save(validate: false)
end

# Updates a single attribute and saves the record.
# This is especially useful for boolean flags on existing records. Also note that
#
# * Validation is skipped.
# * \Callbacks are invoked.
# * updated_at/updated_on column is updated if that column is available.
# * Updates all the attributes that are dirty in this object.
#
# This method raises an ActiveRecord::ActiveRecordError if the
# attribute is marked as readonly.
#
# If any of the <tt>before_*</tt> callbacks throws +:abort+ the action is cancelled
# and #update_attribute! raises ActiveRecord::RecordNotSaved. See
# ActiveRecord::Callbacks for further details.
def update_attribute!(name, value)
name = name.to_s
verify_readonly_attribute(name)
public_send("#{name}=", value)

save!(validate: false)
end

# Updates the attributes of the model from the passed-in hash and saves the
# record, all wrapped in a transaction. If the object is invalid, the saving
# will fail and false will be returned.
Expand Down
63 changes: 63 additions & 0 deletions activerecord/test/cases/persistence_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,69 @@ def test_update_attribute_for_updated_at_on
assert_not_equal prev_month, developer.updated_at
end

def test_update_attribute!
assert_not_predicate Topic.find(1), :approved?
Topic.find(1).update_attribute!("approved", true)
assert_predicate Topic.find(1), :approved?

Topic.find(1).update_attribute!(:approved, false)
assert_not_predicate Topic.find(1), :approved?

Topic.find(1).update_attribute!(:change_approved_before_save, true)
assert_predicate Topic.find(1), :approved?
end

def test_update_attribute_for_readonly_attribute!
minivan = Minivan.find("m1")
assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_attribute!(:color, "black") }
end

def test_update_attribute_with_one_updated!
t = Topic.first
t.update_attribute!(:title, "super_title")
assert_equal "super_title", t.title
assert_not t.changed?, "topic should not have changed"
assert_not 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_updated_at_on!
developer = Developer.find(1)
prev_month = Time.now.prev_month.change(usec: 0)

developer.update_attribute!(:updated_at, prev_month)
assert_equal prev_month, developer.updated_at

developer.update_attribute!(:salary, 80001)
assert_not_equal prev_month, developer.updated_at

developer.reload
assert_not_equal prev_month, developer.updated_at
end

def test_update_attribute_for_aborted_callback!
klass = Class.new(Topic) do
def self.name; "Topic"; end

before_update :throw_abort

def throw_abort
throw(:abort)
end
end

t = klass.create(title: "New Topic", author_name: "Not David")

assert_raises(ActiveRecord::RecordNotSaved) { t.update_attribute!(:title, "super_title") }

t_reloaded = Topic.find(t.id)

assert_equal "New Topic", t_reloaded.title
end

def test_update_column
topic = Topic.find(1)
topic.update_column("approved", true)
Expand Down

0 comments on commit 0638d35

Please sign in to comment.