activerecord_batch_update 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/activerecord_batch_update.rb +118 -0
- metadata +72 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 614292d3e09e070b8d7aff48134d8cee8104dc9f00e96e9deb33b0195bf04858
|
4
|
+
data.tar.gz: 2c4f608cea0f5ae569c76df93c3f3aafed9bc0911c13253d7a2cb02b9c24332d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d5be0abbf08a712b5b5a138b15e37928b9b50c138851651877ac56837f18b884e82b226eaa1c96c1dd1198c8c017bd665c3ac57b6c7e78ecbbfb70f8d0f22443
|
7
|
+
data.tar.gz: ba349d81455f2908dbcddff5da11c4016b8660e13af081dcdfd4cfd55043c35b73b22bd6e221f95638eb6a23e45a6042ae310e8b38de85f7d73e20bf9a677ea8
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
|
5
|
+
module ActiveRecordBatchUpdate
|
6
|
+
extend ::ActiveSupport::Concern
|
7
|
+
|
8
|
+
# Given an array of records with changes,
|
9
|
+
# perform the minimal amount of queries to update
|
10
|
+
# all the records without including unchanged attributes
|
11
|
+
# in the UPDATE statement.
|
12
|
+
#
|
13
|
+
# Do this without INSERT ... ON DUPLICATE KEY UPDATE
|
14
|
+
# which will re-insert the objects if they were deleted in another thread
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def batch_update(entries, columns:, batch_size: 100, validate: true)
|
18
|
+
columns = column_names if columns == :all
|
19
|
+
columns = (Array.wrap(columns).map(&:to_s) + %w[updated_at]).uniq
|
20
|
+
|
21
|
+
entries = entries.select { columns.intersect?(_1.changed) }
|
22
|
+
entries.each { _1.updated_at = Time.current } if has_attribute?('updated_at')
|
23
|
+
entries.each(&:validate!) if validate
|
24
|
+
|
25
|
+
primary_keys = Array.wrap(primary_key).map(&:to_s)
|
26
|
+
|
27
|
+
updated_count = batch_update_statements(
|
28
|
+
entries.map do |entry|
|
29
|
+
(primary_keys + (entry.changed & columns)).to_h { [_1, entry.read_attribute(_1)] }
|
30
|
+
end,
|
31
|
+
update_on: primary_keys,
|
32
|
+
batch_size: batch_size
|
33
|
+
).sum do |sql|
|
34
|
+
connection.exec_update(sql)
|
35
|
+
end
|
36
|
+
|
37
|
+
connection.clear_query_cache if connection.query_cache_enabled
|
38
|
+
|
39
|
+
updated_count
|
40
|
+
end
|
41
|
+
|
42
|
+
def batch_update_statements(entries, update_on: :id, batch_size: 100)
|
43
|
+
update_on = Array.wrap(update_on).map(&:to_s)
|
44
|
+
|
45
|
+
entries.map(&:stringify_keys).group_by { _1.keys.sort! }.sort.flat_map do |(keys, items)|
|
46
|
+
next [] if keys.empty?
|
47
|
+
|
48
|
+
where_clause = where_statement(update_on)
|
49
|
+
update_clause = update_statement(keys - update_on)
|
50
|
+
|
51
|
+
items.each_slice(batch_size).map do |slice|
|
52
|
+
[
|
53
|
+
"WITH \"#{cte_table.name}\" (#{keys.join(', ')})",
|
54
|
+
"AS ( #{values_statement(slice, keys)} )",
|
55
|
+
update_clause,
|
56
|
+
"FROM \"#{cte_table.name}\"",
|
57
|
+
"WHERE #{where_clause}"
|
58
|
+
].join(' ')
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def cte_table
|
66
|
+
@cte_table ||= Arel::Table.new('batch_updates')
|
67
|
+
end
|
68
|
+
|
69
|
+
def values_statement(items, cols)
|
70
|
+
first, *rest = items
|
71
|
+
|
72
|
+
rows = [
|
73
|
+
values_list_casted_item(first, cols),
|
74
|
+
*rest.map { values_list_other_item(_1, cols) }
|
75
|
+
]
|
76
|
+
|
77
|
+
"VALUES #{rows.map { "(#{_1.map(&:to_sql).join(', ')})" }.join(', ')}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def values_list_casted_item(item, cols)
|
81
|
+
cols.map do |col|
|
82
|
+
Arel::Nodes::NamedFunction.new(
|
83
|
+
'CAST',
|
84
|
+
[
|
85
|
+
Arel::Nodes.build_quoted(item[col], arel_table[col]).as(columns_hash[col].sql_type_metadata.sql_type)
|
86
|
+
]
|
87
|
+
)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def values_list_other_item(item, cols)
|
92
|
+
cols.map do |col|
|
93
|
+
Arel::Nodes.build_quoted(item[col], arel_table[col])
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def update_statement(cols)
|
98
|
+
Arel::UpdateManager.new(arel_table).tap do |um|
|
99
|
+
um.set(
|
100
|
+
cols.map do |col|
|
101
|
+
[
|
102
|
+
arel_table[col],
|
103
|
+
cte_table[col]
|
104
|
+
]
|
105
|
+
end
|
106
|
+
)
|
107
|
+
end.to_sql
|
108
|
+
end
|
109
|
+
|
110
|
+
def where_statement(primary_keys)
|
111
|
+
primary_keys.map { arel_table[_1].eq(cte_table[_1]) }.reduce(:and).to_sql
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
ActiveSupport.on_load(:active_record) do
|
117
|
+
include(ActiveRecordBatchUpdate)
|
118
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord_batch_update
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Quentin de Metz
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-10-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '7.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '7.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '7.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '7.0'
|
41
|
+
description: ''
|
42
|
+
email: quentin@pennylane.com
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- lib/activerecord_batch_update.rb
|
48
|
+
homepage: https://rubygems.org/gems/activerecord_batch_update
|
49
|
+
licenses:
|
50
|
+
- MIT
|
51
|
+
metadata:
|
52
|
+
rubygems_mfa_required: 'true'
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 3.3.4
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
requirements: []
|
68
|
+
rubygems_version: 3.5.11
|
69
|
+
signing_key:
|
70
|
+
specification_version: 4
|
71
|
+
summary: Update multiple records with different values in a small number of queries
|
72
|
+
test_files: []
|