1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Touch updated_at when upsert_all modifies a record

- When a user passes `updated_at` to `upsert_all`, the given value is used.
- When a user omits `updated_at`, `upsert_all` touches the timestamp if (but only if) any upserted values differ.

Preserve Rails' ability to generate intelligent cache keys for ActiveRecord when using `upsert_all` frequently to sync imported data.
This commit is contained in:
Bob Lail 2019-11-02 18:37:48 -05:00 committed by Kasper Timm Hansen
parent 31c0dbfe3f
commit 3a8668bd73
No known key found for this signature in database
GPG key ID: 191153215EDA53D8
6 changed files with 59 additions and 0 deletions

View file

@ -510,6 +510,7 @@ module ActiveRecord
sql << " ON DUPLICATE KEY UPDATE #{no_op_column}=#{no_op_column}"
elsif insert.update_duplicates?
sql << " ON DUPLICATE KEY UPDATE "
sql << insert.touch_model_timestamps_unless { |column| "#{column}<=>VALUES(#{column})" }
sql << insert.updatable_columns.map { |column| "#{column}=VALUES(#{column})" }.join(",")
end

View file

@ -450,6 +450,7 @@ module ActiveRecord
sql << " ON CONFLICT #{insert.conflict_target} DO NOTHING"
elsif insert.update_duplicates?
sql << " ON CONFLICT #{insert.conflict_target} DO UPDATE SET "
sql << insert.touch_model_timestamps_unless { |column| "#{insert.model.quoted_table_name}.#{column} IS NOT DISTINCT FROM excluded.#{column}" }
sql << insert.updatable_columns.map { |column| "#{column}=excluded.#{column}" }.join(",")
end

View file

@ -319,6 +319,7 @@ module ActiveRecord
sql << " ON CONFLICT #{insert.conflict_target} DO NOTHING"
elsif insert.update_duplicates?
sql << " ON CONFLICT #{insert.conflict_target} DO UPDATE SET "
sql << insert.touch_model_timestamps_unless { |column| "#{column} IS excluded.#{column}" }
sql << insert.updatable_columns.map { |column| "#{column}=excluded.#{column}" }.join(",")
end

View file

@ -153,9 +153,21 @@ module ActiveRecord
quote_columns(insert_all.updatable_columns)
end
def touch_model_timestamps_unless(&block)
model.send(:timestamp_attributes_for_update_in_model).map do |column_name|
if touch_timestamp_attribute?(column_name)
"#{column_name}=(CASE WHEN (#{updatable_columns.map(&block).join(" AND ")}) THEN #{model.quoted_table_name}.#{column_name} ELSE CURRENT_TIMESTAMP END),"
end
end.compact.join
end
private
attr_reader :connection, :insert_all
def touch_timestamp_attribute?(column_name)
update_duplicates? && !insert_all.updatable_columns.include?(column_name)
end
def columns_list
format_columns(insert_all.keys)
end

View file

@ -273,6 +273,46 @@ class InsertAllTest < ActiveRecord::TestCase
assert_equal ["Out of the Silent Planet", "Perelandra"], Book.where(isbn: "1974522598").order(:name).pluck(:name)
end
def test_upsert_all_does_not_touch_updated_at_when_values_do_not_change
skip unless supports_insert_on_duplicate_update?
updated_at = Time.now.utc - 5.years
Book.insert_all [{ id: 101, name: "Out of the Silent Planet", published_on: Date.new(1938, 4, 1), updated_at: updated_at }]
Book.upsert_all [{ id: 101, name: "Out of the Silent Planet", published_on: Date.new(1938, 4, 1) }]
assert_in_delta updated_at, Book.find(101).updated_at, 1
end
def test_upsert_all_touches_updated_at_and_updated_on_when_values_change
skip unless supports_insert_on_duplicate_update?
Book.insert_all [{ id: 101, name: "Out of the Silent Planet", published_on: Date.new(1938, 4, 1), updated_at: 5.years.ago, updated_on: 5.years.ago }]
Book.upsert_all [{ id: 101, name: "Out of the Silent Planet", published_on: Date.new(1938, 4, 8) }]
assert_equal Time.now.year, Book.find(101).updated_at.year
assert_equal Time.now.year, Book.find(101).updated_on.year
end
def test_upsert_all_uses_given_updated_at_over_implicit_updated_at
skip unless supports_insert_on_duplicate_update?
updated_at = Time.now.utc - 1.year
Book.insert_all [{ id: 101, name: "Out of the Silent Planet", published_on: Date.new(1938, 4, 1), updated_at: 5.years.ago }]
Book.upsert_all [{ id: 101, name: "Out of the Silent Planet", published_on: Date.new(1938, 4, 8), updated_at: updated_at }]
assert_in_delta updated_at, Book.find(101).updated_at, 1
end
def test_upsert_all_uses_given_updated_on_over_implicit_updated_on
skip unless supports_insert_on_duplicate_update?
updated_on = Time.now.utc.to_date - 30
Book.insert_all [{ id: 101, name: "Out of the Silent Planet", published_on: Date.new(1938, 4, 1), updated_on: 5.years.ago }]
Book.upsert_all [{ id: 101, name: "Out of the Silent Planet", published_on: Date.new(1938, 4, 8), updated_on: updated_on }]
assert_equal updated_on, Book.find(101).updated_on
end
def test_insert_all_raises_on_unknown_attribute
assert_raise ActiveRecord::UnknownAttributeError do
Book.insert_all! [{ unknown_attribute: "Test" }]

View file

@ -119,6 +119,10 @@ ActiveRecord::Schema.define do
t.datetime :published_on
t.index [:author_id, :name], unique: true
t.index :isbn, where: "published_on IS NOT NULL", unique: true
t.datetime :created_at
t.datetime :updated_at
t.date :updated_on
end
create_table :booleans, force: true do |t|