mirror of https://github.com/rails/rails
Add support for passing the list of columns to update in upsert_all
#41933 added a new `on_duplicate:` option to `upsert_all`, to allow providing custom SQL update code. This change makes `on_duplicate` admit an array of columns too, so that `upsert_all` only updates those columns when a conflict happens. This allows limiting the list of updated column in a database-agnostic way.
This commit is contained in:
parent
4799156cc3
commit
d133cc40ff
|
@ -1,3 +1,20 @@
|
|||
* Add a new option `:update_only` to `upsert_all` to configure the list of columns to update in case of conflict.
|
||||
|
||||
Before, you could only customize the update SQL sentence via `:on_duplicate`. There is now a new option `:update_only` that lets you provide a list of columns to update in case of conflict:
|
||||
|
||||
```ruby
|
||||
Commodity.upsert_all(
|
||||
[
|
||||
{ id: 2, name: "Copper", price: 4.84 },
|
||||
{ id: 4, name: "Gold", price: 1380.87 },
|
||||
{ id: 6, name: "Aluminium", price: 0.35 }
|
||||
],
|
||||
update_only: [:price] # Only prices will be updated
|
||||
)
|
||||
```
|
||||
|
||||
*Jorge Manrubia*
|
||||
|
||||
* Remove deprecated `ActiveRecord::Result#map!` and `ActiveRecord::Result#collect!`.
|
||||
|
||||
*Rafael Mendonça França*
|
||||
|
|
|
@ -5,22 +5,19 @@ require "active_support/core_ext/enumerable"
|
|||
module ActiveRecord
|
||||
class InsertAll # :nodoc:
|
||||
attr_reader :model, :connection, :inserts, :keys
|
||||
attr_reader :on_duplicate, :returning, :unique_by, :update_sql
|
||||
attr_reader :on_duplicate, :update_only, :returning, :unique_by, :update_sql
|
||||
|
||||
def initialize(model, inserts, on_duplicate:, returning: nil, unique_by: nil, record_timestamps: nil)
|
||||
def initialize(model, inserts, on_duplicate:, update_only: nil, returning: nil, unique_by: nil, record_timestamps: nil)
|
||||
raise ArgumentError, "Empty list of attributes passed" if inserts.blank?
|
||||
|
||||
@model, @connection, @inserts, @keys = model, model.connection, inserts, inserts.first.keys.map(&:to_s)
|
||||
@on_duplicate, @returning, @unique_by = on_duplicate, returning, unique_by
|
||||
@on_duplicate, @update_only, @returning, @unique_by = on_duplicate, update_only, returning, unique_by
|
||||
@record_timestamps = record_timestamps.nil? ? model.record_timestamps : record_timestamps
|
||||
|
||||
disallow_raw_sql!(returning)
|
||||
disallow_raw_sql!(on_duplicate)
|
||||
disallow_raw_sql!(returning)
|
||||
|
||||
if Arel.arel_node?(on_duplicate)
|
||||
@update_sql = on_duplicate
|
||||
@on_duplicate = :update
|
||||
end
|
||||
configure_on_duplicate_update_logic
|
||||
|
||||
if model.scope_attributes?
|
||||
@scope_attributes = model.scope_attributes
|
||||
|
@ -45,7 +42,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def updatable_columns
|
||||
keys - readonly_columns - unique_by_columns
|
||||
@updatable_columns ||= keys - readonly_columns - unique_by_columns
|
||||
end
|
||||
|
||||
def primary_keys
|
||||
|
@ -91,6 +88,24 @@ module ActiveRecord
|
|||
private
|
||||
attr_reader :scope_attributes
|
||||
|
||||
def configure_on_duplicate_update_logic
|
||||
if custom_update_sql_provided? && update_only.present?
|
||||
raise ArgumentError, "You can't set :update_only and provide custom update SQL via :on_duplicate at the same time"
|
||||
end
|
||||
|
||||
if update_only.present?
|
||||
@updatable_columns = Array(update_only)
|
||||
@on_duplicate = :update
|
||||
elsif custom_update_sql_provided?
|
||||
@update_sql = on_duplicate
|
||||
@on_duplicate = :update
|
||||
end
|
||||
end
|
||||
|
||||
def custom_update_sql_provided?
|
||||
@custom_update_sql_provided ||= Arel.arel_node?(on_duplicate)
|
||||
end
|
||||
|
||||
def find_unique_index_for(unique_by)
|
||||
if !connection.supports_insert_conflict_target?
|
||||
return if unique_by.nil?
|
||||
|
|
|
@ -235,6 +235,10 @@ module ActiveRecord
|
|||
# Returns an <tt>ActiveRecord::Result</tt> with its contents based on
|
||||
# <tt>:returning</tt> (see below).
|
||||
#
|
||||
# By default, +upsert_all+ will update all the columns that can be updated when
|
||||
# there is a conflict. These are all the columns except primary keys, read-only
|
||||
# columns, and columns covered by the optional +unique_by+.
|
||||
#
|
||||
# ==== Options
|
||||
#
|
||||
# [:returning]
|
||||
|
@ -268,9 +272,41 @@ module ActiveRecord
|
|||
# Active Record's schema_cache.
|
||||
#
|
||||
# [:on_duplicate]
|
||||
# Specify a custom SQL for updating rows on conflict.
|
||||
# Configure the SQL update sentence that will be used in case of conflict.
|
||||
#
|
||||
# NOTE: in this case you must provide all the columns you want to update by yourself.
|
||||
# NOTE: If you use this option you must provide all the columns you want to update
|
||||
# by yourself.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# Commodity.upsert_all(
|
||||
# [
|
||||
# { id: 2, name: "Copper", price: 4.84 },
|
||||
# { id: 4, name: "Gold", price: 1380.87 },
|
||||
# { id: 6, name: "Aluminium", price: 0.35 }
|
||||
# ],
|
||||
# on_duplicate: Arel.sql("price = GREATEST(commodities.price, EXCLUDED.price)")
|
||||
# )
|
||||
#
|
||||
# See the related +:update_only+ option. Both options can't be used at the same time.
|
||||
#
|
||||
# [:update_only]
|
||||
# Provide a list of column names that will be updated in case of conflict. If not provided,
|
||||
# +upsert_all+ will update all the columns that can be updated. These are all the columns
|
||||
# except primary keys, read-only columns, and columns covered by the optional +unique_by+
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# Commodity.upsert_all(
|
||||
# [
|
||||
# { id: 2, name: "Copper", price: 4.84 },
|
||||
# { id: 4, name: "Gold", price: 1380.87 },
|
||||
# { id: 6, name: "Aluminium", price: 0.35 }
|
||||
# ],
|
||||
# on_duplicate: [:price] # Only prices will be updated
|
||||
# )
|
||||
#
|
||||
# See the related +:on_duplicate+ option. Both options can't be used at the same time.
|
||||
#
|
||||
# [:record_timestamps]
|
||||
# By default, automatic setting of timestamp columns is controlled by
|
||||
|
@ -294,8 +330,8 @@ module ActiveRecord
|
|||
# ], unique_by: :isbn)
|
||||
#
|
||||
# Book.find_by(isbn: "1").title # => "Eloquent Ruby"
|
||||
def upsert_all(attributes, on_duplicate: :update, returning: nil, unique_by: nil, record_timestamps: nil)
|
||||
InsertAll.new(self, attributes, on_duplicate: on_duplicate, returning: returning, unique_by: unique_by, record_timestamps: record_timestamps).execute
|
||||
def upsert_all(attributes, on_duplicate: :update, update_only: nil, returning: nil, unique_by: nil, record_timestamps: nil)
|
||||
InsertAll.new(self, attributes, on_duplicate: on_duplicate, update_only: update_only, returning: returning, unique_by: unique_by, record_timestamps: record_timestamps).execute
|
||||
end
|
||||
|
||||
# Given an attributes hash, +instantiate+ returns a new instance of
|
||||
|
|
|
@ -336,6 +336,31 @@ class InsertAllTest < ActiveRecord::TestCase
|
|||
assert_equal "1974522598", book.isbn, "Should have updated the isbn"
|
||||
end
|
||||
|
||||
def test_passing_both_on_update_and_update_only_will_raise_an_error
|
||||
assert_raises ArgumentError do
|
||||
Book.upsert_all [{ id: 101, name: "Perelandra", author_id: 7, isbn: "1974522598" }], on_duplicate: "NAME=values(name)", update_only: :name
|
||||
end
|
||||
end
|
||||
|
||||
def test_upsert_all_only_updates_the_column_provided_via_update_only
|
||||
Book.upsert_all [{ id: 101, name: "Perelandra", author_id: 7, isbn: "1974522598" }]
|
||||
Book.upsert_all [{ id: 101, name: "Perelandra 2", author_id: 7, isbn: "111111" }], update_only: :name
|
||||
|
||||
book = Book.find(101)
|
||||
assert_equal "Perelandra 2", book.name, "Should have updated the name"
|
||||
assert_equal "1974522598", book.isbn, "Should not have updated the isbn"
|
||||
end
|
||||
|
||||
def test_upsert_all_only_updates_the_list_of_columns_provided_via_update_only
|
||||
Book.upsert_all [{ id: 101, name: "Perelandra", author_id: 7, isbn: "1974522598" }]
|
||||
Book.upsert_all [{ id: 101, name: "Perelandra 2", author_id: 6, isbn: "111111" }], update_only: %i[ name isbn ]
|
||||
|
||||
book = Book.find(101)
|
||||
assert_equal "Perelandra 2", book.name, "Should have updated the name"
|
||||
assert_equal "111111", book.isbn, "Should have updated the isbn"
|
||||
assert_equal 7, book.author_id, "Should not have updated the author_id"
|
||||
end
|
||||
|
||||
def test_upsert_all_does_not_perform_an_upsert_if_a_partial_index_doesnt_apply
|
||||
skip unless supports_insert_on_duplicate_update? && supports_insert_conflict_target? && supports_partial_index?
|
||||
|
||||
|
|
Loading…
Reference in New Issue