atomically 1.0.6 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.travis.yml +13 -2
- data/CHANGELOG.md +3 -0
- data/README.md +87 -25
- data/atomically.gemspec +9 -0
- data/gemfiles/3.2.gemfile +2 -1
- data/gemfiles/4.2.gemfile +2 -1
- data/gemfiles/5.0.gemfile +2 -1
- data/gemfiles/5.1.gemfile +2 -1
- data/gemfiles/5.2.gemfile +2 -1
- data/gemfiles/6.0.gemfile +16 -0
- data/lib/atomically/adapter_check_service.rb +16 -0
- data/lib/atomically/on_duplicate_sql_service.rb +28 -0
- data/lib/atomically/query_service.rb +41 -9
- data/lib/atomically/version.rb +1 -1
- metadata +40 -6
- data/Gemfile.gemfile +0 -12
- data/lib/atomically/update_all_scope.rb +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f11789d3b23462ec0a597434cbd71ad693684fb2
|
4
|
+
data.tar.gz: '0365791ed9e8a5f1b0d035a0db850cc8a5619983'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1bd013da03d5b05cb5dd98545f94cca224efa8815822b0ef1eed537bb1f2f5f5fe05fb82d9aa9fdfefb6c6f676e194a344c6a84254a999d7b3e6e69bc80badbc
|
7
|
+
data.tar.gz: 16335847dbb0540de93a43595a151324109539d9b6d6b728b885e7e83499c7293f6e4ecd5e81d34e41ce2279ab40f67cb10a19eba7529425a602ce4e5c39155f
|
data/.travis.yml
CHANGED
@@ -2,22 +2,30 @@ sudo: false
|
|
2
2
|
language: ruby
|
3
3
|
rvm:
|
4
4
|
- 2.2
|
5
|
-
- 2.
|
5
|
+
- 2.6
|
6
|
+
services:
|
7
|
+
- mysql
|
8
|
+
addons:
|
9
|
+
postgresql: "9.6"
|
6
10
|
env:
|
7
11
|
global:
|
8
12
|
- CC_TEST_REPORTER_ID=12e1facab2e8910c9b9d6b9e6870c5544a5c44a1bef25cc6638fd132aa4af6b4
|
9
13
|
matrix:
|
10
14
|
- DB=mysql
|
15
|
+
- DB=pg
|
11
16
|
gemfile:
|
12
17
|
- gemfiles/3.2.gemfile
|
13
18
|
- gemfiles/4.2.gemfile
|
14
19
|
- gemfiles/5.0.gemfile
|
15
20
|
- gemfiles/5.1.gemfile
|
16
21
|
- gemfiles/5.2.gemfile
|
22
|
+
- gemfiles/6.0.gemfile
|
17
23
|
matrix:
|
18
24
|
exclude:
|
19
25
|
- gemfile: gemfiles/3.2.gemfile
|
20
|
-
rvm: 2.
|
26
|
+
rvm: 2.6
|
27
|
+
- gemfile: gemfiles/6.0.gemfile
|
28
|
+
rvm: 2.2
|
21
29
|
before_install:
|
22
30
|
- gem i rubygems-update -v '<3' && update_rubygems
|
23
31
|
- gem install bundler -v 1.17.3
|
@@ -26,7 +34,10 @@ before_install:
|
|
26
34
|
- chmod +x ./cc-test-reporter
|
27
35
|
- ./cc-test-reporter before-build
|
28
36
|
before_script:
|
37
|
+
- mysql -V
|
29
38
|
- mysql -u root -e 'CREATE DATABASE travis_ci_test;'
|
39
|
+
- psql -c "SELECT version();"
|
40
|
+
- psql -c 'create database travis_ci_test;' -U postgres
|
30
41
|
script:
|
31
42
|
- bundle exec rake test
|
32
43
|
after_script:
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
## Change Log
|
2
2
|
|
3
|
+
### [v1.0.6](https://github.com/khiav223577/atomically/compare/v1.0.5...v1.0.6) 2019/01/28
|
4
|
+
- [#10](https://github.com/khiav223577/atomically/pull/10) `decrement_unsigned_counters` should be able to decrement the field to zero (@khiav223577)
|
5
|
+
|
3
6
|
### [v1.0.5](https://github.com/khiav223577/atomically/compare/v1.0.4...v1.0.5) 2019/01/28
|
4
7
|
- [#9](https://github.com/khiav223577/atomically/pull/9) Implement `decrement_unsigned_counters` (@khiav223577)
|
5
8
|
- [#8](https://github.com/khiav223577/atomically/pull/8) Fix: broken test cases after bundler 2.0 was released (@khiav223577)
|
data/README.md
CHANGED
@@ -8,14 +8,17 @@
|
|
8
8
|
|
9
9
|
`atomically` is a Ruby Gem for you to write atomic query with ease.
|
10
10
|
|
11
|
-
|
11
|
+
All methods are defined in `Atomically::QueryService` instead of defining in `ActiveRecord` directly, in order not to pollute the model instance.
|
12
|
+
|
13
|
+
- Supports Rails 3.2, 4.2, 5.0, 5.1, 5.2, 6.0.
|
14
|
+
- Supports PostgreSQL, MySQL.
|
12
15
|
|
13
16
|
## Table of contents
|
14
17
|
|
15
18
|
1. [Installation](#installation)
|
16
19
|
2. [Methods](#methods)
|
17
20
|
- Relation Methods
|
18
|
-
- [create_or_plus](#create_or_plus-columns-values-on_duplicate_update_columns)
|
21
|
+
- [create_or_plus](#create_or_plus-columns-values-on_duplicate_update_columns-conflict_target)
|
19
22
|
- [pay_all](#pay_all-hash-update_columns-primary_key-id)
|
20
23
|
- [update_all](#update_all-expected_number-updates)
|
21
24
|
- [update_all_and_get_ids](#update_all_and_get_ids-updates)
|
@@ -46,19 +49,35 @@ Or install it yourself as:
|
|
46
49
|
|
47
50
|
Note: ActiveRecord validations and callbacks will **NOT** be triggered when calling below methods.
|
48
51
|
|
49
|
-
### create_or_plus _(columns, values, on_duplicate_update_columns)_
|
52
|
+
### create_or_plus _(columns, values, on_duplicate_update_columns, conflict_target:)_
|
50
53
|
|
51
54
|
Import an array of records. When key is duplicate, plus the old value with new value.
|
52
|
-
It is useful to add `items` to `user` when `user_items` may not exist.
|
55
|
+
It is useful to add `items` to `user` when `user_items` may not exist. (Let `User` and `Item` are many-to-many relationship.)
|
53
56
|
|
54
57
|
#### Parameters
|
55
58
|
|
56
59
|
- First two args (`columns`, `values`) are the same with the [import](https://github.com/zdennis/activerecord-import#columns-and-arrays) method.
|
57
60
|
- `on_duplicate_update_columns` - The column that will be updated on duplicate.
|
61
|
+
- `conflict_target` - Needed only in pg. Specifies which columns have unique index.
|
58
62
|
|
59
63
|
#### Example
|
60
64
|
|
61
65
|
```rb
|
66
|
+
class User < ApplicationRecord
|
67
|
+
has_many :user_items
|
68
|
+
has_many :items, through: :user_items
|
69
|
+
end
|
70
|
+
|
71
|
+
class UserItem < ApplicationRecord
|
72
|
+
belongs_to :user
|
73
|
+
belongs_to :item
|
74
|
+
end
|
75
|
+
|
76
|
+
class Item < ApplicationRecord
|
77
|
+
has_many :user_items
|
78
|
+
has_many :users, through: :user_items
|
79
|
+
end
|
80
|
+
|
62
81
|
user = User.find(2)
|
63
82
|
item1 = Item.find(1)
|
64
83
|
item2 = Item.find(2)
|
@@ -67,44 +86,59 @@ item2 = Item.find(2)
|
|
67
86
|
```rb
|
68
87
|
columns = [:user_id, :item_id, :quantity]
|
69
88
|
values = [[user.id, item1.id, 3], [user.id, item2.id, 2]]
|
70
|
-
on_duplicate_update_columns = [:quantity]
|
71
89
|
|
72
|
-
|
90
|
+
# mysql
|
91
|
+
UserItem.atomically.create_or_plus(columns, values, [:quantity])
|
92
|
+
|
93
|
+
# pg
|
94
|
+
UserItem.atomically.create_or_plus(columns, values, [:quantity], conflict_target: [:user_id, :item_id])
|
73
95
|
```
|
74
96
|
|
75
97
|
before
|
76
98
|
|
77
|
-
![before](https://user-images.githubusercontent.com/4011729/
|
99
|
+
![before](https://user-images.githubusercontent.com/4011729/67365648-95e89480-f5a4-11e9-8147-279385c6f442.png)
|
78
100
|
|
79
101
|
after
|
80
102
|
|
81
|
-
![
|
103
|
+
![after](https://user-images.githubusercontent.com/4011729/67365653-97b25800-f5a4-11e9-8314-8e6ff8d2cd61.png)
|
104
|
+
|
82
105
|
|
83
106
|
#### SQL queries
|
84
107
|
|
85
108
|
```sql
|
109
|
+
# mysql
|
86
110
|
INSERT INTO `user_items` (`user_id`,`item_id`,`quantity`,`created_at`,`updated_at`) VALUES
|
87
111
|
(2,1,3,'2018-11-27 03:44:25','2018-11-27 03:44:25'),
|
88
112
|
(2,2,2,'2018-11-27 03:44:25','2018-11-27 03:44:25')
|
89
|
-
ON DUPLICATE KEY UPDATE
|
113
|
+
ON DUPLICATE KEY UPDATE
|
114
|
+
`quantity` = `quantity` + VALUES(`quantity`)
|
115
|
+
|
116
|
+
# pg
|
117
|
+
INSERT INTO "user_items" ("user_id","item_id","quantity","created_at","updated_at") VALUES
|
118
|
+
(2,1,3,'2018-11-27 03:44:25.847909','2018-11-27 03:44:25.847909'),
|
119
|
+
(2,2,2,'2018-11-27 03:44:25.847909','2018-11-27 03:44:25.847909')
|
120
|
+
ON CONFLICT (user_id, item_id) DO UPDATE SET
|
121
|
+
"quantity" = "user_items"."quantity" + excluded."quantity" RETURNING "id"
|
90
122
|
```
|
91
123
|
|
92
124
|
---
|
93
125
|
### pay_all _(hash, update_columns, primary_key: :id)_
|
94
126
|
|
95
|
-
Reduce the quantity of items and return how many rows and updated if all of them
|
127
|
+
Reduce the quantity of items and return how many rows and updated if all of them are enough.
|
96
128
|
Do nothing and return zero if any of them is not enough.
|
97
129
|
|
98
130
|
#### Parameters
|
99
131
|
|
100
132
|
- `hash` - A hash contains the id of the models as keys and the amount to update the field by as values.
|
101
133
|
- `update_columns` - The column that will be updated.
|
102
|
-
- `primary_key` - Specify the column that `id`(the key of hash)
|
134
|
+
- `primary_key` - Specify the column that `id`(the key of hash) refers to.
|
103
135
|
|
104
136
|
#### Example
|
105
137
|
|
106
138
|
```rb
|
107
139
|
user.user_items.atomically.pay_all({ item1.id => 4, item2.id => 3 }, [:quantity], primary_key: :item_id)
|
140
|
+
# => 2 (if success)
|
141
|
+
# => 0 (if some aren't enough)
|
108
142
|
```
|
109
143
|
|
110
144
|
#### SQL queries
|
@@ -141,21 +175,21 @@ Behaves like [ActiveRecord::Relation#update_all](https://apidock.com/rails/Activ
|
|
141
175
|
|
142
176
|
#### Examples
|
143
177
|
```rb
|
144
|
-
User.where(id: [
|
145
|
-
# => 2
|
178
|
+
User.where(id: [5, 6]).atomically.update_all(2, name: '')
|
179
|
+
# => 2 (success)
|
146
180
|
|
147
|
-
User.where(id: [
|
148
|
-
# => 0
|
181
|
+
User.where(id: [7, 8, 9]).atomically.update_all(2, name: '')
|
182
|
+
# => 0 (fail)
|
149
183
|
```
|
150
184
|
|
151
185
|
#### SQL queries
|
152
186
|
|
153
187
|
```sql
|
154
|
-
# User.where(id: [
|
155
|
-
UPDATE `users` SET `users`.`name` = '' WHERE `users`.`id` IN (
|
188
|
+
# User.where(id: [7, 8, 9]).atomically.update_all(2, name: '')
|
189
|
+
UPDATE `users` SET `users`.`name` = '' WHERE `users`.`id` IN (7, 8, 9) AND (
|
156
190
|
(
|
157
191
|
SELECT COUNT(*) FROM (
|
158
|
-
SELECT `users`.* FROM `users` WHERE `users`.`id` IN (
|
192
|
+
SELECT `users`.* FROM `users` WHERE `users`.`id` IN (7, 8, 9)
|
159
193
|
) subquery
|
160
194
|
) = 2
|
161
195
|
)
|
@@ -164,7 +198,7 @@ UPDATE `users` SET `users`.`name` = '' WHERE `users`.`id` IN (1, 2, 3) AND (
|
|
164
198
|
---
|
165
199
|
### update_all_and_get_ids _(updates)_
|
166
200
|
|
167
|
-
Behaves like [ActiveRecord::Relation#update_all](https://apidock.com/rails/ActiveRecord/Relation/update_all), but return
|
201
|
+
Behaves like [ActiveRecord::Relation#update_all](https://apidock.com/rails/ActiveRecord/Relation/update_all), but return an array of updated records' id instead of the number of updated records.
|
168
202
|
|
169
203
|
|
170
204
|
#### Parameters
|
@@ -175,23 +209,32 @@ Behaves like [ActiveRecord::Relation#update_all](https://apidock.com/rails/Activ
|
|
175
209
|
|
176
210
|
```rb
|
177
211
|
User.where(account: ['moon', 'wolf']).atomically.update_all_and_get_ids('money = money + 1')
|
178
|
-
# => [254, 371]
|
212
|
+
# => [254, 371] (array of updated user ids)
|
213
|
+
|
214
|
+
User.where(account: ['moon', 'wolf']).update_all('money = money + 1')
|
215
|
+
# => 2 (the number of updated records)
|
179
216
|
```
|
180
217
|
|
181
218
|
#### SQL queries
|
182
219
|
|
183
220
|
```sql
|
221
|
+
# mysql
|
184
222
|
BEGIN
|
185
223
|
SET @ids := NULL
|
186
224
|
UPDATE `users` SET money = money + 1 WHERE `users`.`account` IN ('moon', 'wolf') AND ((SELECT @ids := CONCAT_WS(',', `users`.`id`, @ids)))
|
187
225
|
SELECT @ids FROM DUAL
|
188
226
|
COMMIT
|
227
|
+
|
228
|
+
# pg
|
229
|
+
UPDATE 'users' SET money = money + 1 RETURNING id
|
189
230
|
```
|
190
231
|
|
191
232
|
---
|
192
233
|
### update _(attrs, from: :not_set)_
|
193
234
|
|
194
|
-
Updates the attributes of the model from the passed-in hash and saves the record.
|
235
|
+
Updates the attributes of the model from the passed-in hash and saves the record. Return true if update successfully, false otherwise. This method can detect race condition and make sure the model is updated only once.
|
236
|
+
|
237
|
+
The difference between this method and [ActiveRecord#update](https://apidock.com/rails/ActiveRecord/Persistence/update) is that it will add extra WHERE conditions to prevent race condition.
|
195
238
|
|
196
239
|
#### Parameters
|
197
240
|
|
@@ -212,11 +255,30 @@ class Arena < ApplicationRecord
|
|
212
255
|
end
|
213
256
|
```
|
214
257
|
|
258
|
+
Let `arena.closed_at` be nil.
|
259
|
+
|
260
|
+
```rb
|
261
|
+
arena.atomically_close!
|
262
|
+
# => true (if success)
|
263
|
+
# => false (if race condition occurs)
|
264
|
+
```
|
265
|
+
|
266
|
+
The return value can be used to prevent race condition and make sure some piece of code is executed once.
|
267
|
+
|
268
|
+
```rb
|
269
|
+
if arena.atomically_close!
|
270
|
+
# Only one request can pass this check and excete the code here.
|
271
|
+
# You can send rewards, calculate ranking, or fire background job here.
|
272
|
+
# No need to worry about being invoked multiple times.
|
273
|
+
do_something
|
274
|
+
end
|
275
|
+
```
|
276
|
+
|
215
277
|
#### SQL queries
|
216
278
|
|
279
|
+
|
217
280
|
```sql
|
218
281
|
# arena.atomically_close!
|
219
|
-
# (let arena.closed_at to be nil)
|
220
282
|
UPDATE `arenas` SET `arenas`.`closed_at` = '2018-11-27 03:44:25', `updated_at` = '2018-11-27 03:44:25'
|
221
283
|
WHERE `arenas`.`id` = 1752 AND `arenas`.`closed_at` IS NULL
|
222
284
|
|
@@ -244,12 +306,12 @@ user.money
|
|
244
306
|
# => 100
|
245
307
|
|
246
308
|
user.atomically.decrement_unsigned_counters(money: 10)
|
247
|
-
# => true
|
309
|
+
# => true (success)
|
248
310
|
user.reload.money
|
249
311
|
# => 90
|
250
312
|
|
251
313
|
user.atomically.decrement_unsigned_counters(money: 999)
|
252
|
-
# => false
|
314
|
+
# => false (fail)
|
253
315
|
user.reload.money
|
254
316
|
# => 90
|
255
317
|
```
|
@@ -258,7 +320,7 @@ user.reload.money
|
|
258
320
|
|
259
321
|
```sql
|
260
322
|
# user.atomically.decrement_unsigned_counters(money: 140)
|
261
|
-
UPDATE `users` SET money = money - 140 WHERE `users`.`id` = 1 AND (money
|
323
|
+
UPDATE `users` SET money = money - 140 WHERE `users`.`id` = 1 AND (money >= 140)
|
262
324
|
```
|
263
325
|
|
264
326
|
## Development
|
data/atomically.gemspec
CHANGED
@@ -26,16 +26,25 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.bindir = 'exe'
|
27
27
|
spec.executables = spec.files.grep(%r{^exe/}){|f| File.basename(f) }
|
28
28
|
spec.require_paths = ['lib']
|
29
|
+
spec.metadata = {
|
30
|
+
'homepage_uri' => 'https://github.com/khiav223577/atomically',
|
31
|
+
'changelog_uri' => 'https://github.com/khiav223577/atomically/blob/master/CHANGELOG.md',
|
32
|
+
'source_code_uri' => 'https://github.com/khiav223577/atomically',
|
33
|
+
'documentation_uri' => 'https://www.rubydoc.info/gems/atomically',
|
34
|
+
'bug_tracker_uri' => 'https://github.com/khiav223577/atomically/issues',
|
35
|
+
}
|
29
36
|
|
30
37
|
spec.add_development_dependency 'bundler', '>= 1.17', '< 3.x'
|
31
38
|
spec.add_development_dependency 'rake', '~> 12.0'
|
32
39
|
spec.add_development_dependency 'sqlite3', '~> 1.3'
|
33
40
|
spec.add_development_dependency 'minitest', '~> 5.0'
|
34
41
|
spec.add_development_dependency 'mysql2', '>= 0.3'
|
42
|
+
spec.add_development_dependency "pg", "~> 0.18"
|
35
43
|
spec.add_development_dependency 'pluck_all', '>= 2.0.3'
|
36
44
|
spec.add_development_dependency 'timecop', '~> 0.9.1'
|
37
45
|
|
38
46
|
spec.add_dependency 'activerecord', '>= 3'
|
39
47
|
spec.add_dependency 'activerecord-import', '>= 0.27.0'
|
40
48
|
spec.add_dependency 'rails_or', '>= 1.1.8'
|
49
|
+
spec.add_dependency 'update_all_scope', '~> 0.1.0'
|
41
50
|
end
|
data/gemfiles/3.2.gemfile
CHANGED
@@ -8,9 +8,10 @@ group :test do
|
|
8
8
|
when 'postgres' ; gem 'pg', '~> 0.18'
|
9
9
|
end
|
10
10
|
gem 'simplecov'
|
11
|
-
gem '
|
11
|
+
gem 'i18n', '< 1.6'
|
12
12
|
gem 'pluck_all', '>= 2.0.3'
|
13
13
|
gem 'timecop', '~> 0.9.1'
|
14
|
+
gem 'update_all_scope', '~> 0.1.0'
|
14
15
|
end
|
15
16
|
|
16
17
|
gemspec path: '../'
|
data/gemfiles/4.2.gemfile
CHANGED
@@ -8,9 +8,10 @@ group :test do
|
|
8
8
|
when 'postgres' ; gem 'pg', '~> 0.18'
|
9
9
|
end
|
10
10
|
gem 'simplecov'
|
11
|
-
gem '
|
11
|
+
gem 'i18n', '< 1.6'
|
12
12
|
gem 'pluck_all', '>= 2.0.3'
|
13
13
|
gem 'timecop', '~> 0.9.1'
|
14
|
+
gem 'update_all_scope', '~> 0.1.0'
|
14
15
|
end
|
15
16
|
|
16
17
|
gemspec path: '../'
|
data/gemfiles/5.0.gemfile
CHANGED
@@ -8,9 +8,10 @@ group :test do
|
|
8
8
|
when 'postgres' ; gem 'pg', '~> 0.18'
|
9
9
|
end
|
10
10
|
gem 'simplecov'
|
11
|
-
gem '
|
11
|
+
gem 'i18n', '< 1.6'
|
12
12
|
gem 'pluck_all', '>= 2.0.3'
|
13
13
|
gem 'timecop', '~> 0.9.1'
|
14
|
+
gem 'update_all_scope', '~> 0.1.0'
|
14
15
|
end
|
15
16
|
|
16
17
|
gemspec path: '../'
|
data/gemfiles/5.1.gemfile
CHANGED
@@ -8,9 +8,10 @@ group :test do
|
|
8
8
|
when 'postgres' ; gem 'pg', '~> 0.18'
|
9
9
|
end
|
10
10
|
gem 'simplecov'
|
11
|
-
gem '
|
11
|
+
gem 'i18n', '< 1.6'
|
12
12
|
gem 'pluck_all', '>= 2.0.3'
|
13
13
|
gem 'timecop', '~> 0.9.1'
|
14
|
+
gem 'update_all_scope', '~> 0.1.0'
|
14
15
|
end
|
15
16
|
|
16
17
|
gemspec path: '../'
|
data/gemfiles/5.2.gemfile
CHANGED
@@ -8,9 +8,10 @@ group :test do
|
|
8
8
|
when 'postgres' ; gem 'pg', '~> 0.18'
|
9
9
|
end
|
10
10
|
gem 'simplecov'
|
11
|
-
gem '
|
11
|
+
gem 'i18n', '< 1.6'
|
12
12
|
gem 'pluck_all', '>= 2.0.3'
|
13
13
|
gem 'timecop', '~> 0.9.1'
|
14
|
+
gem 'update_all_scope', '~> 0.1.0'
|
14
15
|
end
|
15
16
|
|
16
17
|
gemspec path: '../'
|
@@ -0,0 +1,16 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'activerecord', '~> 6.0.0'
|
4
|
+
|
5
|
+
group :test do
|
6
|
+
case ENV['DB']
|
7
|
+
when 'mysql' ; gem 'mysql2', '0.5.1'
|
8
|
+
when 'postgres' ; gem 'pg', '~> 0.18'
|
9
|
+
end
|
10
|
+
gem 'simplecov'
|
11
|
+
gem 'pluck_all', '>= 2.0.4'
|
12
|
+
gem 'timecop', '~> 0.9.1'
|
13
|
+
gem 'update_all_scope', '~> 0.1.0'
|
14
|
+
end
|
15
|
+
|
16
|
+
gemspec path: '../'
|
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
class Atomically::AdapterCheckService
|
3
|
+
def initialize(klass)
|
4
|
+
@klass = klass
|
5
|
+
end
|
6
|
+
|
7
|
+
def pg?
|
8
|
+
return false if not defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
|
9
|
+
return @klass.connection.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
|
10
|
+
end
|
11
|
+
|
12
|
+
def mysql?
|
13
|
+
return false if not defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
|
14
|
+
return @klass.connection.is_a?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Atomically::OnDuplicateSqlService
|
4
|
+
def initialize(klass, columns)
|
5
|
+
@klass = klass
|
6
|
+
@columns = columns
|
7
|
+
end
|
8
|
+
|
9
|
+
def mysql_quote_columns_for_plus
|
10
|
+
return @columns.map do |column|
|
11
|
+
quoted_column = quote_column(column)
|
12
|
+
next "#{quoted_column} = #{quoted_column} + VALUES(#{quoted_column})"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def pg_quote_columns_for_plus
|
17
|
+
return @columns.map do |column|
|
18
|
+
quoted_column = quote_column(column)
|
19
|
+
next "#{quoted_column} = #{@klass.quoted_table_name}.#{quoted_column} + excluded.#{quoted_column}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def quote_column(column)
|
26
|
+
@klass.connection.quote_column_name(column)
|
27
|
+
end
|
28
|
+
end
|
@@ -2,20 +2,24 @@
|
|
2
2
|
|
3
3
|
require 'activerecord-import'
|
4
4
|
require 'rails_or'
|
5
|
-
require '
|
5
|
+
require 'update_all_scope'
|
6
|
+
require 'atomically/on_duplicate_sql_service'
|
7
|
+
require 'atomically/adapter_check_service'
|
6
8
|
require 'atomically/patches/clear_attribute_changes' if not ActiveModel::Dirty.method_defined?(:clear_attribute_changes) and not ActiveModel::Dirty.private_method_defined?(:clear_attribute_changes)
|
7
9
|
require 'atomically/patches/none' if not ActiveRecord::Base.respond_to?(:none)
|
8
10
|
require 'atomically/patches/from' if Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new('4.0.0')
|
9
11
|
|
10
12
|
class Atomically::QueryService
|
13
|
+
DEFAULT_CONFLICT_TARGETS = [:id].freeze
|
14
|
+
|
11
15
|
def initialize(klass, relation: nil, model: nil)
|
12
16
|
@klass = klass
|
13
17
|
@relation = relation || @klass
|
14
18
|
@model = model
|
15
19
|
end
|
16
20
|
|
17
|
-
def create_or_plus(columns, data, update_columns)
|
18
|
-
@klass.import(columns, data, on_duplicate_key_update: on_duplicate_key_plus_sql(update_columns))
|
21
|
+
def create_or_plus(columns, data, update_columns, conflict_target: DEFAULT_CONFLICT_TARGETS)
|
22
|
+
@klass.import(columns, data, on_duplicate_key_update: on_duplicate_key_plus_sql(update_columns, conflict_target))
|
19
23
|
end
|
20
24
|
|
21
25
|
def pay_all(hash, update_columns, primary_key: :id) # { id => pay_count }
|
@@ -30,8 +34,13 @@ class Atomically::QueryService
|
|
30
34
|
end
|
31
35
|
|
32
36
|
raw_when_sql = hash.map{|id, pay_count| "WHEN #{sanitize(id)} THEN #{sanitize(-pay_count)}" }.join("\n")
|
37
|
+
no_var_in_sql = true if update_columns.size == 1 or db_is_pg?
|
33
38
|
update_sqls = update_columns.map.with_index do |column, idx|
|
34
|
-
|
39
|
+
if no_var_in_sql
|
40
|
+
value = "(\nCASE #{quote_column(primary_key)}\n#{raw_when_sql}\nEND)"
|
41
|
+
else
|
42
|
+
value = idx == 0 ? "(@change := \nCASE #{quote_column(primary_key)}\n#{raw_when_sql}\nEND)" : '@change'
|
43
|
+
end
|
35
44
|
next "#{column} = #{column} + #{value}"
|
36
45
|
end
|
37
46
|
|
@@ -62,20 +71,43 @@ class Atomically::QueryService
|
|
62
71
|
end
|
63
72
|
|
64
73
|
def update_all_and_get_ids(*args)
|
74
|
+
if db_is_pg?
|
75
|
+
scope = UpdateAllScope::UpdateAllScope.new(model: @model, relation: @relation.where(''))
|
76
|
+
scope.update(*args)
|
77
|
+
return @klass.connection.execute("#{scope.to_sql} RETURNING id", "#{@klass} Update All").map{|s| s['id'].to_i }
|
78
|
+
end
|
79
|
+
|
65
80
|
ids = nil
|
66
|
-
id_column =
|
81
|
+
id_column = quote_column_with_table(:id)
|
67
82
|
@klass.transaction do
|
68
83
|
@relation.connection.execute('SET @ids := NULL')
|
69
84
|
@relation.where("(SELECT @ids := CONCAT_WS(',', #{id_column}, @ids))").update_all(*args) # 撈出有真的被更新的 id,用逗號串在一起
|
70
|
-
ids = @klass.from(nil).pluck('@ids').first
|
85
|
+
ids = @klass.from(nil).pluck(Arel.sql('@ids')).first
|
71
86
|
end
|
72
87
|
return ids.try{|s| s.split(',').map(&:to_i).uniq.sort } || [] # 將 id 從字串取出來 @id 的格式範例: '1,4,12'
|
73
88
|
end
|
74
89
|
|
75
90
|
private
|
76
91
|
|
77
|
-
def
|
78
|
-
|
92
|
+
def db_is_pg?
|
93
|
+
Atomically::AdapterCheckService.new(@klass).pg?
|
94
|
+
end
|
95
|
+
|
96
|
+
def db_is_mysql?
|
97
|
+
Atomically::AdapterCheckService.new(@klass).mysql?
|
98
|
+
end
|
99
|
+
|
100
|
+
def on_duplicate_key_plus_sql(columns, conflict_target)
|
101
|
+
service = Atomically::OnDuplicateSqlService.new(@klass, columns)
|
102
|
+
return service.mysql_quote_columns_for_plus.join(', ') if db_is_mysql?
|
103
|
+
return {
|
104
|
+
conflict_target: conflict_target,
|
105
|
+
columns: service.pg_quote_columns_for_plus.join(', ')
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
def quote_column_with_table(column)
|
110
|
+
"#{@klass.quoted_table_name}.#{quote_column(column)}"
|
79
111
|
end
|
80
112
|
|
81
113
|
def quote_column(column)
|
@@ -103,7 +135,7 @@ class Atomically::QueryService
|
|
103
135
|
|
104
136
|
def open_update_all_scope(&block)
|
105
137
|
return 0 if @model == nil
|
106
|
-
scope = UpdateAllScope.new(model: @model)
|
138
|
+
scope = UpdateAllScope::UpdateAllScope.new(model: @model)
|
107
139
|
scope.instance_exec(&block)
|
108
140
|
return scope.do_query!
|
109
141
|
end
|
data/lib/atomically/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: atomically
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- khiav reoy
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-10-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -86,6 +86,20 @@ dependencies:
|
|
86
86
|
- - ">="
|
87
87
|
- !ruby/object:Gem::Version
|
88
88
|
version: '0.3'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: pg
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0.18'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0.18'
|
89
103
|
- !ruby/object:Gem::Dependency
|
90
104
|
name: pluck_all
|
91
105
|
requirement: !ruby/object:Gem::Requirement
|
@@ -156,6 +170,20 @@ dependencies:
|
|
156
170
|
- - ">="
|
157
171
|
- !ruby/object:Gem::Version
|
158
172
|
version: 1.1.8
|
173
|
+
- !ruby/object:Gem::Dependency
|
174
|
+
name: update_all_scope
|
175
|
+
requirement: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - "~>"
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: 0.1.0
|
180
|
+
type: :runtime
|
181
|
+
prerelease: false
|
182
|
+
version_requirements: !ruby/object:Gem::Requirement
|
183
|
+
requirements:
|
184
|
+
- - "~>"
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: 0.1.0
|
159
187
|
description: Adds commonly useful atomic SQL statements to ActiveRecord to avoid race
|
160
188
|
condition.
|
161
189
|
email:
|
@@ -169,7 +197,6 @@ files:
|
|
169
197
|
- ".travis.yml"
|
170
198
|
- CHANGELOG.md
|
171
199
|
- CODE_OF_CONDUCT.md
|
172
|
-
- Gemfile.gemfile
|
173
200
|
- LICENSE
|
174
201
|
- LICENSE.txt
|
175
202
|
- README.md
|
@@ -182,18 +209,25 @@ files:
|
|
182
209
|
- gemfiles/5.0.gemfile
|
183
210
|
- gemfiles/5.1.gemfile
|
184
211
|
- gemfiles/5.2.gemfile
|
212
|
+
- gemfiles/6.0.gemfile
|
185
213
|
- lib/atomically.rb
|
186
214
|
- lib/atomically/active_record/extension.rb
|
215
|
+
- lib/atomically/adapter_check_service.rb
|
216
|
+
- lib/atomically/on_duplicate_sql_service.rb
|
187
217
|
- lib/atomically/patches/clear_attribute_changes.rb
|
188
218
|
- lib/atomically/patches/from.rb
|
189
219
|
- lib/atomically/patches/none.rb
|
190
220
|
- lib/atomically/query_service.rb
|
191
|
-
- lib/atomically/update_all_scope.rb
|
192
221
|
- lib/atomically/version.rb
|
193
222
|
homepage: https://github.com/khiav223577/atomically
|
194
223
|
licenses:
|
195
224
|
- MIT
|
196
|
-
metadata:
|
225
|
+
metadata:
|
226
|
+
homepage_uri: https://github.com/khiav223577/atomically
|
227
|
+
changelog_uri: https://github.com/khiav223577/atomically/blob/master/CHANGELOG.md
|
228
|
+
source_code_uri: https://github.com/khiav223577/atomically
|
229
|
+
documentation_uri: https://www.rubydoc.info/gems/atomically
|
230
|
+
bug_tracker_uri: https://github.com/khiav223577/atomically/issues
|
197
231
|
post_install_message:
|
198
232
|
rdoc_options: []
|
199
233
|
require_paths:
|
@@ -210,7 +244,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
210
244
|
version: '0'
|
211
245
|
requirements: []
|
212
246
|
rubyforge_project:
|
213
|
-
rubygems_version: 2.
|
247
|
+
rubygems_version: 2.6.14
|
214
248
|
signing_key:
|
215
249
|
specification_version: 4
|
216
250
|
summary: Adds commonly useful atomic SQL statements to ActiveRecord to avoid race
|
data/Gemfile.gemfile
DELETED
@@ -1,28 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class UpdateAllScope
|
4
|
-
def initialize(model: nil, relation: nil)
|
5
|
-
@queries = []
|
6
|
-
@relation = relation || model.class.where(id: model.id)
|
7
|
-
end
|
8
|
-
|
9
|
-
def where(*args)
|
10
|
-
@relation = @relation.where(*args)
|
11
|
-
return self
|
12
|
-
end
|
13
|
-
|
14
|
-
def update(query, *binding_values)
|
15
|
-
args = binding_values.size > 0 ? [[query, *binding_values]] : [query]
|
16
|
-
@queries << klass.send(:sanitize_sql_for_assignment, *args)
|
17
|
-
return self
|
18
|
-
end
|
19
|
-
|
20
|
-
def do_query!
|
21
|
-
return 0 if @queries.empty?
|
22
|
-
return @relation.update_all(@queries.join(','))
|
23
|
-
end
|
24
|
-
|
25
|
-
def klass
|
26
|
-
@relation.klass
|
27
|
-
end
|
28
|
-
end
|