Skip to content

Commit 5cec7ce

Browse files
kamiposimi
andcommitted
Support bulk insert/upsert on relation to preserve scope values
This allows to work `author.books.insert_all` as expected. Co-authored-by: Josef Šimánek <[email protected]>
1 parent 587bfea commit 5cec7ce

File tree

5 files changed

+89
-2
lines changed

5 files changed

+89
-2
lines changed

activerecord/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
* Support bulk insert/upsert on relation to preserve scope values.
2+
3+
*Josef Šimánek*, *Ryuta Kamizono*
4+
15
* Preserve column comment value on changing column name on MySQL.
26

37
*Islam Taha*

activerecord/lib/active_record/association_relation.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@ def create!(attributes = nil, &block)
3131
scoping { @association.create!(attributes, &block) }
3232
end
3333

34+
%w(insert insert_all insert! insert_all! upsert upsert_all).each do |method|
35+
class_eval <<~RUBY
36+
def #{method}(attributes, **kwargs)
37+
if @association.reflection.through_reflection?
38+
raise ArgumentError, "Bulk insert or upsert is currently not supported for has_many through association"
39+
end
40+
41+
scoping { klass.#{method}(attributes, **kwargs) }
42+
end
43+
RUBY
44+
end
45+
3446
private
3547
def exec_queries
3648
super do |record|

activerecord/lib/active_record/associations/collection_proxy.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1096,7 +1096,9 @@ def reset_scope # :nodoc:
10961096
SpawnMethods,
10971097
].flat_map { |klass|
10981098
klass.public_instance_methods(false)
1099-
} - self.public_instance_methods(false) - [:select] + [:scoping, :values]
1099+
} - self.public_instance_methods(false) - [:select] + [
1100+
:scoping, :values, :insert, :insert_all, :insert!, :insert_all!, :upsert, :upsert_all
1101+
]
11001102

11011103
delegate(*delegate_methods, to: :scope)
11021104

activerecord/lib/active_record/insert_all.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,15 @@ class InsertAll # :nodoc:
1010
def initialize(model, inserts, on_duplicate:, returning: nil, unique_by: nil)
1111
raise ArgumentError, "Empty list of attributes passed" if inserts.blank?
1212

13-
@model, @connection, @inserts, @keys = model, model.connection, inserts, inserts.first.keys.map(&:to_s).to_set
13+
@model, @connection, @inserts, @keys = model, model.connection, inserts, inserts.first.keys.map(&:to_s)
1414
@on_duplicate, @returning, @unique_by = on_duplicate, returning, unique_by
1515

16+
if model.scope_attributes?
17+
@scope_attributes = model.scope_attributes
18+
@keys |= @scope_attributes.keys
19+
end
20+
@keys = @keys.to_set
21+
1622
@returning = (connection.supports_insert_returning? ? primary_keys : false) if @returning.nil?
1723
@returning = false if @returning == []
1824

@@ -49,6 +55,8 @@ def update_duplicates?
4955
def map_key_with_value
5056
inserts.map do |attributes|
5157
attributes = attributes.stringify_keys
58+
attributes.merge!(scope_attributes) if scope_attributes
59+
5260
verify_attributes(attributes)
5361

5462
keys.map do |key|
@@ -58,6 +66,8 @@ def map_key_with_value
5866
end
5967

6068
private
69+
attr_reader :scope_attributes
70+
6171
def find_unique_index_for(unique_by)
6272
name_or_columns = unique_by || model.primary_key
6373
match = Array(name_or_columns).map(&:to_s)

activerecord/test/cases/insert_all_test.rb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
# frozen_string_literal: true
22

33
require "cases/helper"
4+
require "models/author"
45
require "models/book"
56
require "models/speedometer"
7+
require "models/subscription"
8+
require "models/subscriber"
69

710
class ReadonlyNameBook < Book
811
attr_readonly :name
@@ -341,6 +344,62 @@ def test_insert_all_with_enum_values
341344
assert_equal ["published", "proposed"], Book.where(isbn: ["1234566", "1234567"]).order(:id).pluck(:status)
342345
end
343346

347+
def test_insert_all_on_relation
348+
author = Author.create!(name: "Jimmy")
349+
350+
assert_difference "author.books.count", +1 do
351+
author.books.insert_all!([{ name: "My little book", isbn: "1974522598" }])
352+
end
353+
end
354+
355+
def test_insert_all_on_relation_precedence
356+
author = Author.create!(name: "Jimmy")
357+
second_author = Author.create!(name: "Bob")
358+
359+
assert_difference "author.books.count", +1 do
360+
author.books.insert_all!([{ name: "My little book", isbn: "1974522598", author_id: second_author.id }])
361+
end
362+
end
363+
364+
def test_insert_all_create_with
365+
assert_difference "Book.where(format: 'X').count", +2 do
366+
Book.create_with(format: "X").insert_all!([ { name: "A" }, { name: "B" } ])
367+
end
368+
end
369+
370+
def test_insert_all_has_many_through
371+
book = Book.first
372+
assert_raise(ArgumentError) { book.subscribers.insert_all!([ { nick: "Jimmy" } ]) }
373+
end
374+
375+
def test_upsert_all_on_relation
376+
author = Author.create!(name: "Jimmy")
377+
378+
assert_difference "author.books.count", +1 do
379+
author.books.upsert_all([{ name: "My little book", isbn: "1974522598" }])
380+
end
381+
end
382+
383+
def test_upsert_all_on_relation_precedence
384+
author = Author.create!(name: "Jimmy")
385+
second_author = Author.create!(name: "Bob")
386+
387+
assert_difference "author.books.count", +1 do
388+
author.books.upsert_all([{ name: "My little book", isbn: "1974522598", author_id: second_author.id }])
389+
end
390+
end
391+
392+
def test_upsert_all_create_with
393+
assert_difference "Book.where(format: 'X').count", +2 do
394+
Book.create_with(format: "X").upsert_all([ { name: "A" }, { name: "B" } ])
395+
end
396+
end
397+
398+
def test_upsert_all_has_many_through
399+
book = Book.first
400+
assert_raise(ArgumentError) { book.subscribers.upsert_all([ { nick: "Jimmy" } ]) }
401+
end
402+
344403
private
345404
def capture_log_output
346405
output = StringIO.new

0 commit comments

Comments
 (0)