Skip to content

Commit 3963a13

Browse files
authored
Merge pull request rails#51455 from moofkit/update-columns-touch
Add `touch` option to `#update_columns` and `#update_column` methods
2 parents 79711de + 1419ca5 commit 3963a13

File tree

3 files changed

+86
-3
lines changed

3 files changed

+86
-3
lines changed

activerecord/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
* Add `:touch` option to `update_column`/`update_columns` methods.
2+
3+
```ruby
4+
# Will update :updated_at/:updated_on alongside :nice column.
5+
user.update_column(:nice, true, touch: true)
6+
7+
# Will update :updated_at/:updated_on alongside :last_ip column
8+
user.update_columns(last_ip: request.remote_ip, touch: true)
9+
```
10+
11+
*Dmitrii Ivliev*
12+
113
* Optimize Active Record batching further when using ranges.
214

315
Tested on a PostgreSQL table with 10M records and batches of 10k records, the generation

activerecord/lib/active_record/persistence.rb

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -582,8 +582,8 @@ def update!(attributes)
582582
end
583583

584584
# Equivalent to <code>update_columns(name => value)</code>.
585-
def update_column(name, value)
586-
update_columns(name => value)
585+
def update_column(name, value, touch: nil)
586+
update_columns(name => value, :touch => touch)
587587
end
588588

589589
# Updates the attributes directly in the database issuing an UPDATE SQL
@@ -597,11 +597,25 @@ def update_column(name, value)
597597
#
598598
# * \Validations are skipped.
599599
# * \Callbacks are skipped.
600-
# * +updated_at+/+updated_on+ are not updated.
600+
# * +updated_at+/+updated_on+ are updated if the +touch+ option is set to +true+.
601601
# * However, attributes are serialized with the same rules as ActiveRecord::Relation#update_all
602602
#
603603
# This method raises an ActiveRecord::ActiveRecordError when called on new
604604
# objects, or when at least one of the attributes is marked as readonly.
605+
#
606+
# ==== Parameters
607+
#
608+
# * <tt>:touch</tt> option - Touch the timestamp columns when updating.
609+
# * If attribute names are passed, they are updated along with +updated_at+/+updated_on+ attributes.
610+
#
611+
# ==== Examples
612+
#
613+
# # Update a single attribute.
614+
# user.update_columns(last_request_at: Time.current)
615+
#
616+
# # Update with touch option.
617+
# user.update_columns(last_request_at: Time.current, touch: true)
618+
605619
def update_columns(attributes)
606620
raise ActiveRecordError, "cannot update a new record" if new_record?
607621
raise ActiveRecordError, "cannot update a destroyed record" if destroyed?
@@ -613,6 +627,15 @@ def update_columns(attributes)
613627
verify_readonly_attribute(name) || name
614628
end
615629

630+
touch = attributes.delete("touch")
631+
if touch
632+
names = touch if touch != true
633+
names = Array.wrap(names)
634+
options = names.extract_options!
635+
touch_updates = self.class.touch_attributes_with_time(*names, **options)
636+
attributes.with_defaults!(touch_updates) unless touch_updates.empty?
637+
end
638+
616639
update_constraints = _query_constraints_hash
617640
attributes = attributes.each_with_object({}) do |(k, v), h|
618641
h[k] = @attributes.write_cast_value(k, v)

activerecord/test/cases/persistence_test.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,6 +1139,16 @@ def test_update_column
11391139
assert_not_predicate topic, :approved?
11401140
end
11411141

1142+
def test_update_column_touch_option
1143+
topic = Topic.find(1)
1144+
1145+
assert_changes -> { topic.updated_at } do
1146+
travel(1.second) do
1147+
topic.update_column(:title, "super_title", touch: true)
1148+
end
1149+
end
1150+
end
1151+
11421152
def test_update_column_should_not_use_setter_method
11431153
dev = Developer.find(1)
11441154
dev.instance_eval { def salary=(value); write_attribute(:salary, value * 2); end }
@@ -1230,6 +1240,44 @@ def test_update_columns
12301240
assert_equal "Sebastian Topic", topic.title
12311241
end
12321242

1243+
def test_update_columns_touch_option_updates_timestamps
1244+
topic = Topic.find(1)
1245+
1246+
assert_changes -> { topic.updated_at } do
1247+
travel(1.second) do
1248+
topic.update_columns(title: "super_title", touch: true)
1249+
end
1250+
end
1251+
end
1252+
1253+
def test_update_columns_touch_option_explicit_column_names
1254+
topic = Topic.find(1)
1255+
1256+
assert_changes -> { [topic.updated_at, topic.written_on] } do
1257+
travel(1.second) do
1258+
topic.update_columns(title: "super_title", touch: :written_on)
1259+
end
1260+
end
1261+
end
1262+
1263+
def test_update_columns_touch_option_not_overwrite_explicit_attribute
1264+
topic = Topic.find(1)
1265+
new_updated_at = Date.parse("2024-03-31 12:00:00")
1266+
1267+
assert_changes -> { topic.updated_at }, to: new_updated_at do
1268+
topic.update_columns(title: "super_title", updated_at: new_updated_at, touch: true)
1269+
end
1270+
end
1271+
1272+
def test_update_columns_touch_option_not_overwrite_explicit_attribute_with_string_key
1273+
topic = Topic.find(1)
1274+
new_updated_at = Date.parse("2024-03-31 12:00:00")
1275+
1276+
assert_changes -> { topic.updated_at }, to: new_updated_at do
1277+
topic.update_columns(title: "super_title", "updated_at" => new_updated_at, touch: true)
1278+
end
1279+
end
1280+
12331281
def test_update_columns_should_not_use_setter_method
12341282
dev = Developer.find(1)
12351283
dev.instance_eval { def salary=(value); write_attribute(:salary, value * 2); end }

0 commit comments

Comments
 (0)