dynamoid 3.10.0 → 3.12.1
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 +4 -4
- data/CHANGELOG.md +37 -1
- data/README.md +268 -8
- data/dynamoid.gemspec +4 -4
- data/lib/dynamoid/adapter.rb +1 -1
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/filter_expression_convertor.rb +53 -18
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +5 -4
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/projection_expression_convertor.rb +9 -7
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +1 -1
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +1 -1
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/transact.rb +31 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +17 -5
- data/lib/dynamoid/components.rb +1 -0
- data/lib/dynamoid/config.rb +3 -0
- data/lib/dynamoid/criteria/chain.rb +74 -21
- data/lib/dynamoid/criteria/where_conditions.rb +13 -6
- data/lib/dynamoid/dirty.rb +97 -11
- data/lib/dynamoid/dumping.rb +39 -17
- data/lib/dynamoid/errors.rb +30 -3
- data/lib/dynamoid/fields.rb +13 -3
- data/lib/dynamoid/finders.rb +44 -23
- data/lib/dynamoid/loadable.rb +1 -0
- data/lib/dynamoid/persistence/inc.rb +35 -19
- data/lib/dynamoid/persistence/item_updater_with_casting_and_dumping.rb +36 -0
- data/lib/dynamoid/persistence/item_updater_with_dumping.rb +33 -0
- data/lib/dynamoid/persistence/save.rb +29 -14
- data/lib/dynamoid/persistence/update_fields.rb +23 -8
- data/lib/dynamoid/persistence/update_validations.rb +3 -3
- data/lib/dynamoid/persistence/upsert.rb +22 -8
- data/lib/dynamoid/persistence.rb +184 -28
- data/lib/dynamoid/transaction_read/find.rb +137 -0
- data/lib/dynamoid/transaction_read.rb +146 -0
- data/lib/dynamoid/transaction_write/base.rb +47 -0
- data/lib/dynamoid/transaction_write/create.rb +49 -0
- data/lib/dynamoid/transaction_write/delete_with_instance.rb +65 -0
- data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +64 -0
- data/lib/dynamoid/transaction_write/destroy.rb +84 -0
- data/lib/dynamoid/transaction_write/item_updater.rb +55 -0
- data/lib/dynamoid/transaction_write/save.rb +169 -0
- data/lib/dynamoid/transaction_write/update_attributes.rb +46 -0
- data/lib/dynamoid/transaction_write/update_fields.rb +239 -0
- data/lib/dynamoid/transaction_write/upsert.rb +106 -0
- data/lib/dynamoid/transaction_write.rb +673 -0
- data/lib/dynamoid/type_casting.rb +3 -1
- data/lib/dynamoid/undumping.rb +13 -2
- data/lib/dynamoid/validations.rb +8 -5
- data/lib/dynamoid/version.rb +1 -1
- data/lib/dynamoid.rb +8 -0
- metadata +21 -5
@@ -0,0 +1,673 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dynamoid/transaction_write/create'
|
4
|
+
require 'dynamoid/transaction_write/delete_with_primary_key'
|
5
|
+
require 'dynamoid/transaction_write/delete_with_instance'
|
6
|
+
require 'dynamoid/transaction_write/destroy'
|
7
|
+
require 'dynamoid/transaction_write/save'
|
8
|
+
require 'dynamoid/transaction_write/update_fields'
|
9
|
+
require 'dynamoid/transaction_write/update_attributes'
|
10
|
+
require 'dynamoid/transaction_write/upsert'
|
11
|
+
require 'dynamoid/transaction_write/item_updater'
|
12
|
+
|
13
|
+
module Dynamoid
|
14
|
+
# The class +TransactionWrite+ provides means to perform multiple modifying
|
15
|
+
# operations in transaction, that is atomically, so that either all of them
|
16
|
+
# succeed, or all of them fail.
|
17
|
+
#
|
18
|
+
# The persisting methods are supposed to be as close as possible to their
|
19
|
+
# non-transactional counterparts like +.create+, +#save+ and +#delete+:
|
20
|
+
#
|
21
|
+
# user = User.new()
|
22
|
+
# payment = Payment.find(1)
|
23
|
+
#
|
24
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
25
|
+
# t.save! user
|
26
|
+
# t.create! Account, name: 'A'
|
27
|
+
# t.delete payment
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# The only difference is that the methods are called on a transaction
|
31
|
+
# instance and a model or a model class should be specified.
|
32
|
+
#
|
33
|
+
# So +user.save!+ becomes +t.save!(user)+, +Account.create!(name: 'A')+
|
34
|
+
# becomes +t.create!(Account, name: 'A')+, and +payment.delete+ becomes
|
35
|
+
# +t.delete(payment)+.
|
36
|
+
#
|
37
|
+
# A transaction can be used without a block. This way a transaction instance
|
38
|
+
# should be instantiated and committed manually with +#commit+ method:
|
39
|
+
#
|
40
|
+
# t = Dynamoid::TransactionWrite.new
|
41
|
+
#
|
42
|
+
# t.save! user
|
43
|
+
# t.create! Account, name: 'A'
|
44
|
+
# t.delete payment
|
45
|
+
#
|
46
|
+
# t.commit
|
47
|
+
#
|
48
|
+
# Some persisting methods are intentionally not available in a transaction,
|
49
|
+
# e.g. +.update+ and +.update!+ that simply call +.find+ and
|
50
|
+
# +#update_attributes+ methods. These methods perform multiple operations so
|
51
|
+
# cannot be implemented in a transactional atomic way.
|
52
|
+
#
|
53
|
+
#
|
54
|
+
# ### DynamoDB's transactions
|
55
|
+
#
|
56
|
+
# The main difference between DynamoDB transactions and a common interface is
|
57
|
+
# that DynamoDB's transactions are executed in batch. So in Dynamoid no
|
58
|
+
# changes are actually persisted when some transactional method (e.g+ `#save+) is
|
59
|
+
# called. All the changes are persisted at the end.
|
60
|
+
#
|
61
|
+
# A +TransactWriteItems+ DynamoDB operation is used (see
|
62
|
+
# [documentation](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html)
|
63
|
+
# for details).
|
64
|
+
#
|
65
|
+
#
|
66
|
+
# ### Callbacks
|
67
|
+
#
|
68
|
+
# The transactional methods support +before_+, +after_+ and +around_+
|
69
|
+
# callbacks to the extend the non-transactional methods support them.
|
70
|
+
#
|
71
|
+
# There is important difference - a transactional method runs callbacks
|
72
|
+
# immediately (even +after_+ ones) when it is called before changes are
|
73
|
+
# actually persisted. So code in +after_+ callbacks does not see observes
|
74
|
+
# them in DynamoDB and so for.
|
75
|
+
#
|
76
|
+
# When a callback aborts persisting of a model or a model is invalid then
|
77
|
+
# transaction is not aborted and may commit successfully.
|
78
|
+
#
|
79
|
+
#
|
80
|
+
# ### Transaction rollback
|
81
|
+
#
|
82
|
+
# A transaction is rolled back on DynamoDB's side automatically when:
|
83
|
+
# - an ongoing operation is in the process of updating the same item.
|
84
|
+
# - there is insufficient provisioned capacity for the transaction to be completed.
|
85
|
+
# - an item size becomes too large (bigger than 400 KB), a local secondary index (LSI) becomes too large, or a similar validation error occurs because of changes made by the transaction.
|
86
|
+
# - the aggregate size of the items in the transaction exceeds 4 MB.
|
87
|
+
# - there is a user error, such as an invalid data format.
|
88
|
+
#
|
89
|
+
# A transaction can be interrupted simply by an exception raised within a
|
90
|
+
# block. As far as no changes are actually persisted before the +#commit+
|
91
|
+
# method call - there is nothing to undo on the DynamoDB's site.
|
92
|
+
#
|
93
|
+
# Raising +Dynamoid::Errors::Rollback+ exception leads to interrupting a
|
94
|
+
# transation and it isn't propogated:
|
95
|
+
#
|
96
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
97
|
+
# t.save! user
|
98
|
+
# t.create! Account, name: 'A'
|
99
|
+
#
|
100
|
+
# if user.is_admin?
|
101
|
+
# raise Dynamoid::Errors::Rollback
|
102
|
+
# end
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# When a transaction is successfully committed or rolled backed -
|
106
|
+
# corresponding +#after_commit+ or +#after_rollback+ callbacks are run for
|
107
|
+
# each involved model.
|
108
|
+
class TransactionWrite
|
109
|
+
def self.execute
|
110
|
+
transaction = new
|
111
|
+
|
112
|
+
begin
|
113
|
+
yield transaction
|
114
|
+
rescue StandardError => e
|
115
|
+
transaction.rollback
|
116
|
+
|
117
|
+
unless e.is_a?(Dynamoid::Errors::Rollback)
|
118
|
+
raise e
|
119
|
+
end
|
120
|
+
else
|
121
|
+
transaction.commit
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def initialize
|
126
|
+
@actions = []
|
127
|
+
end
|
128
|
+
|
129
|
+
# Persist all the changes.
|
130
|
+
#
|
131
|
+
# transaction = Dynamoid::TransactionWrite.new
|
132
|
+
# # ...
|
133
|
+
# transaction.commit
|
134
|
+
def commit
|
135
|
+
actions_to_commit = @actions.reject(&:aborted?).reject(&:skipped?)
|
136
|
+
return if actions_to_commit.empty?
|
137
|
+
|
138
|
+
action_requests = actions_to_commit.map(&:action_request)
|
139
|
+
Dynamoid.adapter.transact_write_items(action_requests)
|
140
|
+
actions_to_commit.each(&:on_commit)
|
141
|
+
|
142
|
+
nil
|
143
|
+
rescue Aws::Errors::ServiceError
|
144
|
+
run_on_rollback_callbacks
|
145
|
+
raise
|
146
|
+
end
|
147
|
+
|
148
|
+
def rollback
|
149
|
+
run_on_rollback_callbacks
|
150
|
+
end
|
151
|
+
|
152
|
+
# Create new model or persist changes in already existing one.
|
153
|
+
#
|
154
|
+
# Run the validation and callbacks. Returns +true+ if saving is successful
|
155
|
+
# and +false+ otherwise.
|
156
|
+
#
|
157
|
+
# user = User.new
|
158
|
+
#
|
159
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
160
|
+
# t.save!(user)
|
161
|
+
# end
|
162
|
+
#
|
163
|
+
# Validation can be skipped with +validate: false+ option:
|
164
|
+
#
|
165
|
+
# user = User.new(age: -1)
|
166
|
+
#
|
167
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
168
|
+
# t.save!(user, validate: false)
|
169
|
+
# end
|
170
|
+
#
|
171
|
+
# +save!+ by default sets timestamps attributes - +created_at+ and
|
172
|
+
# +updated_at+ when creates new model and updates +updated_at+ attribute
|
173
|
+
# when updates already existing one.
|
174
|
+
#
|
175
|
+
# If a model is new and hash key (+id+ by default) is not assigned yet
|
176
|
+
# it was assigned implicitly with random UUID value.
|
177
|
+
#
|
178
|
+
# When a model is not persisted - its id should have unique value.
|
179
|
+
# Otherwise a transaction will be rolled back.
|
180
|
+
#
|
181
|
+
# Raises +Dynamoid::Errors::MissingHashKey+ if a model is already persisted
|
182
|
+
# and a partition key has value +nil+ and raises
|
183
|
+
# +Dynamoid::Errors::MissingRangeKey+ if a sort key is required but has
|
184
|
+
# value +nil+.
|
185
|
+
#
|
186
|
+
# There are the following differences between transactional and
|
187
|
+
# non-transactional +#save!+:
|
188
|
+
# - transactional +#save!+ doesn't support the +:touch+ option
|
189
|
+
# - transactional +#save!+ doesn't support +lock_version+ attribute so it
|
190
|
+
# will not be incremented and will not be checked to detect concurrent
|
191
|
+
# modification of a model and +Dynamoid::Errors::StaleObjectError+
|
192
|
+
# exception will not be raised
|
193
|
+
# - transactional +#save!+ doesn't raise +Dynamoid::Errors::RecordNotUnique+
|
194
|
+
# at saving new model when primary key is already used. A generic
|
195
|
+
# +Aws::DynamoDB::Errors::TransactionCanceledException+ is raised instead.
|
196
|
+
# - transactional +save!+ doesn't raise
|
197
|
+
# +Dynamoid::Errors::StaleObjectError+ when a model that is being updated
|
198
|
+
# was concurrently deleted
|
199
|
+
# - a table isn't created lazily if it doesn't exist yet
|
200
|
+
#
|
201
|
+
# @param model [Dynamoid::Document] a model
|
202
|
+
# @param options [Hash] (optional)
|
203
|
+
# @option options [true|false] :validate validate a model or not - +true+ by default (optional)
|
204
|
+
# @return [true|false] Whether saving successful or not
|
205
|
+
def save!(model, **options)
|
206
|
+
action = Dynamoid::TransactionWrite::Save.new(model, **options, raise_error: true)
|
207
|
+
register_action action
|
208
|
+
end
|
209
|
+
|
210
|
+
# Create new model or persist changes in already existing one.
|
211
|
+
#
|
212
|
+
# Run the validation and callbacks. Raise
|
213
|
+
# +Dynamoid::Errors::DocumentNotValid+ unless this object is valid.
|
214
|
+
#
|
215
|
+
# user = User.new
|
216
|
+
#
|
217
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
218
|
+
# t.save(user)
|
219
|
+
# end
|
220
|
+
#
|
221
|
+
# Validation can be skipped with +validate: false+ option:
|
222
|
+
#
|
223
|
+
# user = User.new(age: -1)
|
224
|
+
#
|
225
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
226
|
+
# t.save(user, validate: false)
|
227
|
+
# end
|
228
|
+
#
|
229
|
+
# +save+ by default sets timestamps attributes - +created_at+ and
|
230
|
+
# +updated_at+ when creates new model and updates +updated_at+ attribute
|
231
|
+
# when updates already existing one.
|
232
|
+
#
|
233
|
+
# If a model is new and hash key (+id+ by default) is not assigned yet
|
234
|
+
# it was assigned implicitly with random UUID value.
|
235
|
+
#
|
236
|
+
# When a model is not persisted - its id should have unique value.
|
237
|
+
# Otherwise a transaction will be rolled back.
|
238
|
+
#
|
239
|
+
# Raises +Dynamoid::Errors::MissingHashKey+ if a model is already persisted
|
240
|
+
# and a partition key has value +nil+ and raises
|
241
|
+
# +Dynamoid::Errors::MissingRangeKey+ if a sort key is required but has
|
242
|
+
# value +nil+.
|
243
|
+
#
|
244
|
+
# There are the following differences between transactional and
|
245
|
+
# non-transactional +#save+:
|
246
|
+
# - transactional +#save+ doesn't support the +:touch+ option
|
247
|
+
# - transactional +#save+ doesn't support +lock_version+ attribute so it
|
248
|
+
# will not be incremented and will not be checked to detect concurrent
|
249
|
+
# modification of a model and +Dynamoid::Errors::StaleObjectError+
|
250
|
+
# exception will not be raised
|
251
|
+
# - transactional +#save+ doesn't raise +Dynamoid::Errors::RecordNotUnique+
|
252
|
+
# at saving new model when primary key is already used. A generic
|
253
|
+
# +Aws::DynamoDB::Errors::TransactionCanceledException+ is raised instead.
|
254
|
+
# - transactional +save+ doesn't raise +Dynamoid::Errors::StaleObjectError+
|
255
|
+
# when a model that is being updated was concurrently deleted
|
256
|
+
# - a table isn't created lazily if it doesn't exist yet
|
257
|
+
#
|
258
|
+
# @param model [Dynamoid::Document] a model
|
259
|
+
# @param options [Hash] (optional)
|
260
|
+
# @option options [true|false] :validate validate a model or not - +true+ by default (optional)
|
261
|
+
# @return [true|false] Whether saving successful or not
|
262
|
+
def save(model, **options)
|
263
|
+
action = Dynamoid::TransactionWrite::Save.new(model, **options, raise_error: false)
|
264
|
+
register_action action
|
265
|
+
end
|
266
|
+
|
267
|
+
# Create a model.
|
268
|
+
#
|
269
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
270
|
+
# t.create!(User, name: 'A')
|
271
|
+
# end
|
272
|
+
#
|
273
|
+
# Accepts both Hash and Array of Hashes and can create several models.
|
274
|
+
#
|
275
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
276
|
+
# t.create!(User, [{name: 'A'}, {name: 'B'}, {name: 'C'}])
|
277
|
+
# end
|
278
|
+
#
|
279
|
+
# Instantiates a model and pass it into an optional block to set other
|
280
|
+
# attributes.
|
281
|
+
#
|
282
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
283
|
+
# t.create!(User, name: 'A') do |user|
|
284
|
+
# user.initialize_roles
|
285
|
+
# end
|
286
|
+
# end
|
287
|
+
#
|
288
|
+
# Validates model and runs callbacks.
|
289
|
+
#
|
290
|
+
# Raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is required but
|
291
|
+
# not specified or has value +nil+.
|
292
|
+
#
|
293
|
+
# There are the following differences between transactional and
|
294
|
+
# non-transactional +#create+:
|
295
|
+
# - transactional +#create!+ doesn't support +lock_version+ attribute so it
|
296
|
+
# will not be incremented and will not be checked to detect concurrent
|
297
|
+
# modification of a model and +Dynamoid::Errors::StaleObjectError+
|
298
|
+
# exception will not be raised
|
299
|
+
# - transactional +#create!+ doesn't raise +Dynamoid::Errors::RecordNotUnique+
|
300
|
+
# at saving new model when primary key is already used. A generic
|
301
|
+
# +Aws::DynamoDB::Errors::TransactionCanceledException+ is raised instead.
|
302
|
+
# - a table isn't created lazily if it doesn't exist yet
|
303
|
+
#
|
304
|
+
# @param model_class [Class] a model class which should be instantiated
|
305
|
+
# @param attributes [Hash|Array<Hash>] attributes of a model
|
306
|
+
# @param block [Proc] a block to process a model after initialization
|
307
|
+
# @return [Dynamoid::Document] a model that was instantiated but not yet persisted
|
308
|
+
def create!(model_class, attributes = {}, &block)
|
309
|
+
if attributes.is_a? Array
|
310
|
+
attributes.map do |attr|
|
311
|
+
action = Dynamoid::TransactionWrite::Create.new(model_class, attr, raise_error: true, &block)
|
312
|
+
register_action action
|
313
|
+
end
|
314
|
+
else
|
315
|
+
action = Dynamoid::TransactionWrite::Create.new(model_class, attributes, raise_error: true, &block)
|
316
|
+
register_action action
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
# Create a model.
|
321
|
+
#
|
322
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
323
|
+
# t.create(User, name: 'A')
|
324
|
+
# end
|
325
|
+
#
|
326
|
+
# Accepts both Hash and Array of Hashes and can create several models.
|
327
|
+
#
|
328
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
329
|
+
# t.create(User, [{name: 'A'}, {name: 'B'}, {name: 'C'}])
|
330
|
+
# end
|
331
|
+
#
|
332
|
+
# Instantiates a model and pass it into an optional block to set other
|
333
|
+
# attributes.
|
334
|
+
#
|
335
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
336
|
+
# t.create(User, name: 'A') do |user|
|
337
|
+
# user.initialize_roles
|
338
|
+
# end
|
339
|
+
# end
|
340
|
+
#
|
341
|
+
# Validates model and runs callbacks.
|
342
|
+
#
|
343
|
+
# Raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is required but
|
344
|
+
# not specified or has value +nil+.
|
345
|
+
#
|
346
|
+
# There are the following differences between transactional and
|
347
|
+
# non-transactional +#create+:
|
348
|
+
# - transactional +#create+ doesn't support +lock_version+ attribute so it
|
349
|
+
# will not be incremented and will not be checked to detect concurrent
|
350
|
+
# modification of a model and +Dynamoid::Errors::StaleObjectError+
|
351
|
+
# exception will not be raised
|
352
|
+
# - transactional +#create+ doesn't raise +Dynamoid::Errors::RecordNotUnique+
|
353
|
+
# at saving new model when primary key is already used. A generic
|
354
|
+
# +Aws::DynamoDB::Errors::TransactionCanceledException+ is raised instead.
|
355
|
+
# - a table isn't created lazily if it doesn't exist yet
|
356
|
+
#
|
357
|
+
# @param model_class [Class] a model class which should be instantiated
|
358
|
+
# @param attributes [Hash|Array<Hash>] attributes of a model
|
359
|
+
# @param block [Proc] a block to process a model after initialization
|
360
|
+
# @return [Dynamoid::Document] a model that was instantiated but not yet persisted
|
361
|
+
def create(model_class, attributes = {}, &block)
|
362
|
+
if attributes.is_a? Array
|
363
|
+
attributes.map do |attr|
|
364
|
+
action = Dynamoid::TransactionWrite::Create.new(model_class, attr, raise_error: false, &block)
|
365
|
+
register_action action
|
366
|
+
end
|
367
|
+
else
|
368
|
+
action = Dynamoid::TransactionWrite::Create.new(model_class, attributes, raise_error: false, &block)
|
369
|
+
register_action action
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
# Update an existing document or create a new one.
|
374
|
+
#
|
375
|
+
# If a document with specified hash and range keys doesn't exist it
|
376
|
+
# creates a new document with specified attributes. Doesn't run
|
377
|
+
# validations and callbacks.
|
378
|
+
#
|
379
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
380
|
+
# t.upsert(User, '1', age: 26)
|
381
|
+
# end
|
382
|
+
#
|
383
|
+
# If range key is declared for a model it should be passed as well:
|
384
|
+
#
|
385
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
386
|
+
# t.upsert(User, '1', 'Tylor', age: 26)
|
387
|
+
# end
|
388
|
+
#
|
389
|
+
# Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
|
390
|
+
# attributes is not declared in the model class.
|
391
|
+
#
|
392
|
+
# Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
|
393
|
+
# +nil+ and +Dynamoid::Errors::MissingRangeKey+ if a sort key is required
|
394
|
+
# but has value +nil+.
|
395
|
+
#
|
396
|
+
# There are the following differences between transactional and
|
397
|
+
# non-transactional +#upsert+:
|
398
|
+
# - transactional +#upsert+ doesn't support conditions (that's +if+ and
|
399
|
+
# +unless_exists+ options)
|
400
|
+
# - transactional +#upsert+ doesn't return a document that was updated or
|
401
|
+
# created
|
402
|
+
#
|
403
|
+
# @param model_class [Class] a model class
|
404
|
+
# @param hash_key [Scalar value] hash key value
|
405
|
+
# @param range_key [Scalar value] range key value (optional)
|
406
|
+
# @param attributes [Hash]
|
407
|
+
# @return [nil]
|
408
|
+
def upsert(model_class, hash_key, range_key = nil, attributes) # rubocop:disable Style/OptionalArguments
|
409
|
+
action = Dynamoid::TransactionWrite::Upsert.new(model_class, hash_key, range_key, attributes)
|
410
|
+
register_action action
|
411
|
+
end
|
412
|
+
|
413
|
+
# Update document.
|
414
|
+
#
|
415
|
+
# Doesn't run validations and callbacks.
|
416
|
+
#
|
417
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
418
|
+
# t.update_fields(User, '1', age: 26)
|
419
|
+
# end
|
420
|
+
#
|
421
|
+
# If range key is declared for a model it should be passed as well:
|
422
|
+
#
|
423
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
424
|
+
# t.update_fields(User, '1', 'Tylor', age: 26)
|
425
|
+
# end
|
426
|
+
#
|
427
|
+
# Updates can also be performed in a block.
|
428
|
+
#
|
429
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
430
|
+
# t.update_fields(User, 1) do |u|
|
431
|
+
# u.add(article_count: 1)
|
432
|
+
# u.delete(favorite_colors: 'green')
|
433
|
+
# u.set(age: 27, last_name: 'Tylor')
|
434
|
+
# end
|
435
|
+
# end
|
436
|
+
#
|
437
|
+
# Operation +add+ just adds a value for numeric attributes and join
|
438
|
+
# collections if attribute is a set.
|
439
|
+
#
|
440
|
+
# t.update_fields(User, 1) do |u|
|
441
|
+
# u.add(age: 1, followers_count: 5)
|
442
|
+
# u.add(hobbies: ['skying', 'climbing'])
|
443
|
+
# end
|
444
|
+
#
|
445
|
+
# Operation +delete+ is applied to collection attribute types and
|
446
|
+
# substructs one collection from another.
|
447
|
+
#
|
448
|
+
# t.update_fields(User, 1) do |u|
|
449
|
+
# u.delete(hobbies: ['skying'])
|
450
|
+
# end
|
451
|
+
#
|
452
|
+
# Operation +set+ just changes an attribute value:
|
453
|
+
#
|
454
|
+
# t.update_fields(User, 1) do |u|
|
455
|
+
# u.set(age: 21)
|
456
|
+
# end
|
457
|
+
#
|
458
|
+
# Operation +remove+ removes one or more attributes from an item.
|
459
|
+
#
|
460
|
+
# t.update_fields(User, 1) do |u|
|
461
|
+
# u.remove(:age)
|
462
|
+
# end
|
463
|
+
#
|
464
|
+
# All the operations work like +ADD+, +DELETE+, +REMOVE+, and +SET+ actions supported
|
465
|
+
# by +UpdateExpression+
|
466
|
+
# {parameter}[https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html]
|
467
|
+
# of +UpdateItem+ operation.
|
468
|
+
#
|
469
|
+
# It's atomic operations. So adding or deleting elements in a collection
|
470
|
+
# or incrementing or decrementing a numeric field is atomic and does not
|
471
|
+
# interfere with other write requests.
|
472
|
+
#
|
473
|
+
# Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
|
474
|
+
# attributes is not declared in the model class.
|
475
|
+
#
|
476
|
+
# Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
|
477
|
+
# +nil+ and +Dynamoid::Errors::MissingRangeKey+ if a sort key is required
|
478
|
+
# but has value +nil+.
|
479
|
+
#
|
480
|
+
# There are the following differences between transactional and
|
481
|
+
# non-transactional +#update_fields+:
|
482
|
+
# - transactional +#update_fields+ doesn't support conditions (that's +if+
|
483
|
+
# and +unless_exists+ options)
|
484
|
+
# - transactional +#update_fields+ doesn't return a document that was
|
485
|
+
# updated or created
|
486
|
+
#
|
487
|
+
# @param model_class [Class] a model class
|
488
|
+
# @param hash_key [Scalar value] hash key value
|
489
|
+
# @param range_key [Scalar value] range key value (optional)
|
490
|
+
# @param attributes [Hash]
|
491
|
+
# @return [nil]
|
492
|
+
def update_fields(model_class, hash_key, range_key = nil, attributes = nil, &block)
|
493
|
+
# given no attributes, but there may be a block
|
494
|
+
if range_key.is_a?(Hash) && !attributes
|
495
|
+
attributes = range_key
|
496
|
+
range_key = nil
|
497
|
+
end
|
498
|
+
|
499
|
+
action = Dynamoid::TransactionWrite::UpdateFields.new(model_class, hash_key, range_key, attributes, &block)
|
500
|
+
register_action action
|
501
|
+
end
|
502
|
+
|
503
|
+
# Update multiple attributes at once.
|
504
|
+
#
|
505
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
506
|
+
# t.update_attributes(user, age: 27, last_name: 'Tylor')
|
507
|
+
# end
|
508
|
+
#
|
509
|
+
# Returns +true+ if saving is successful and +false+
|
510
|
+
# otherwise.
|
511
|
+
#
|
512
|
+
# Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
|
513
|
+
# +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
|
514
|
+
# required but has value +nil+.
|
515
|
+
#
|
516
|
+
# There are the following differences between transactional and
|
517
|
+
# non-transactional +#update_attributes+:
|
518
|
+
# - transactional +#update_attributes+ doesn't support +lock_version+ attribute so it
|
519
|
+
# will not be incremented and will not be checked to detect concurrent
|
520
|
+
# modification of a model and +Dynamoid::Errors::StaleObjectError+
|
521
|
+
# exception will not be raised
|
522
|
+
# - transactional +update_attributes+ doesn't raise
|
523
|
+
# +Dynamoid::Errors::StaleObjectError+ when a model that is being updated
|
524
|
+
# was concurrently deleted
|
525
|
+
# - a table isn't created lazily if it doesn't exist yet
|
526
|
+
#
|
527
|
+
# @param model [Dynamoid::Document] a model
|
528
|
+
# @param attributes [Hash] a hash of attributes to update
|
529
|
+
# @return [true|false] Whether updating successful or not
|
530
|
+
def update_attributes(model, attributes)
|
531
|
+
action = Dynamoid::TransactionWrite::UpdateAttributes.new(model, attributes, raise_error: false)
|
532
|
+
register_action action
|
533
|
+
end
|
534
|
+
|
535
|
+
# Update multiple attributes at once.
|
536
|
+
#
|
537
|
+
# Returns +true+ if saving is successful and +false+
|
538
|
+
# otherwise.
|
539
|
+
#
|
540
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
541
|
+
# t.update_attributes(user, age: 27, last_name: 'Tylor')
|
542
|
+
# end
|
543
|
+
#
|
544
|
+
# Raises a +Dynamoid::Errors::DocumentNotValid+ exception if some vaidation
|
545
|
+
# fails.
|
546
|
+
#
|
547
|
+
# Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
|
548
|
+
# +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
|
549
|
+
# required but has value +nil+.
|
550
|
+
#
|
551
|
+
# There are the following differences between transactional and
|
552
|
+
# non-transactional +#update_attributes!+:
|
553
|
+
# - transactional +#update_attributes!+ doesn't support +lock_version+
|
554
|
+
# attribute so it will not be incremented and will not be checked to detect
|
555
|
+
# concurrent modification of a model and
|
556
|
+
# +Dynamoid::Errors::StaleObjectError+ exception will not be raised
|
557
|
+
# - transactional +update_attributes!+ doesn't raise
|
558
|
+
# +Dynamoid::Errors::StaleObjectError+ when a model that is being updated
|
559
|
+
# was concurrently deleted
|
560
|
+
# - a table isn't created lazily if it doesn't exist yet
|
561
|
+
#
|
562
|
+
# @param model [Dynamoid::Document] a model
|
563
|
+
# @param attributes [Hash] a hash of attributes to update
|
564
|
+
def update_attributes!(model, attributes)
|
565
|
+
action = Dynamoid::TransactionWrite::UpdateAttributes.new(model, attributes, raise_error: true)
|
566
|
+
register_action action
|
567
|
+
end
|
568
|
+
|
569
|
+
# Delete a model.
|
570
|
+
#
|
571
|
+
# Can be called either with a model:
|
572
|
+
#
|
573
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
574
|
+
# t.delete(user)
|
575
|
+
# end
|
576
|
+
#
|
577
|
+
# or with a primary key:
|
578
|
+
#
|
579
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
580
|
+
# t.delete(User, user_id)
|
581
|
+
# end
|
582
|
+
#
|
583
|
+
# Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
|
584
|
+
# +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
|
585
|
+
# required but has value +nil+.
|
586
|
+
#
|
587
|
+
# There are the following differences between transactional and
|
588
|
+
# non-transactional +#delete+: TBD
|
589
|
+
# - transactional +#delete+ doesn't support +lock_version+ attribute so it
|
590
|
+
# will not be incremented and will not be checked to detect concurrent
|
591
|
+
# modification of a model and +Dynamoid::Errors::StaleObjectError+
|
592
|
+
# exception will not be raised
|
593
|
+
# - transactional +#delete+ doesn't disassociate a model from associated ones
|
594
|
+
# if there is any
|
595
|
+
#
|
596
|
+
# @param model_or_model_class [Class|Dynamoid::Document] either model or model class
|
597
|
+
# @param hash_key [Scalar value] hash key value
|
598
|
+
# @param range_key [Scalar value] range key value (optional)
|
599
|
+
# @return [Dynamoid::Document] self
|
600
|
+
def delete(model_or_model_class, hash_key = nil, range_key = nil)
|
601
|
+
action = if model_or_model_class.is_a? Class
|
602
|
+
Dynamoid::TransactionWrite::DeleteWithPrimaryKey.new(model_or_model_class, hash_key, range_key)
|
603
|
+
else
|
604
|
+
Dynamoid::TransactionWrite::DeleteWithInstance.new(model_or_model_class)
|
605
|
+
end
|
606
|
+
register_action action
|
607
|
+
end
|
608
|
+
|
609
|
+
# Delete a model.
|
610
|
+
#
|
611
|
+
# Runs callbacks.
|
612
|
+
#
|
613
|
+
# Raises +Dynamoid::Errors::RecordNotDestroyed+ exception if model deleting
|
614
|
+
# failed (e.g. aborted by a callback).
|
615
|
+
#
|
616
|
+
# Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
|
617
|
+
# +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
|
618
|
+
# required but has value +nil+.
|
619
|
+
#
|
620
|
+
# There are the following differences between transactional and
|
621
|
+
# non-transactional +#destroy!+:
|
622
|
+
# - transactional +#destroy!+ doesn't support +lock_version+ attribute so it
|
623
|
+
# will not be incremented and will not be checked to detect concurrent
|
624
|
+
# modification of a model and +Dynamoid::Errors::StaleObjectError+
|
625
|
+
# exception will not be raised
|
626
|
+
# - transactional +#destroy!+ doesn't disassociate a model from associated ones
|
627
|
+
# if there are association declared in the model class
|
628
|
+
#
|
629
|
+
# @param model [Dynamoid::Document] a model
|
630
|
+
# @return [Dynamoid::Document|false] returns self if destoying is succefull, +false+ otherwise
|
631
|
+
def destroy!(model)
|
632
|
+
action = Dynamoid::TransactionWrite::Destroy.new(model, raise_error: true)
|
633
|
+
register_action action
|
634
|
+
end
|
635
|
+
|
636
|
+
# Delete a model.
|
637
|
+
#
|
638
|
+
# Runs callbacks.
|
639
|
+
#
|
640
|
+
# Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
|
641
|
+
# +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
|
642
|
+
# required but has value +nil+.
|
643
|
+
#
|
644
|
+
# There are the following differences between transactional and
|
645
|
+
# non-transactional +#destroy+:
|
646
|
+
# - transactional +#destroy+ doesn't support +lock_version+ attribute so it
|
647
|
+
# will not be incremented and will not be checked to detect concurrent
|
648
|
+
# modification of a model and +Dynamoid::Errors::StaleObjectError+
|
649
|
+
# exception will not be raised
|
650
|
+
# - transactional +#destroy+ doesn't disassociate a model from associated ones
|
651
|
+
# if there are association declared in the model class
|
652
|
+
#
|
653
|
+
# @param model [Dynamoid::Document] a model
|
654
|
+
# @return [Dynamoid::Document] self
|
655
|
+
def destroy(model)
|
656
|
+
action = Dynamoid::TransactionWrite::Destroy.new(model, raise_error: false)
|
657
|
+
register_action action
|
658
|
+
end
|
659
|
+
|
660
|
+
private
|
661
|
+
|
662
|
+
def register_action(action)
|
663
|
+
@actions << action
|
664
|
+
action.on_registration
|
665
|
+
action.observable_by_user_result
|
666
|
+
end
|
667
|
+
|
668
|
+
def run_on_rollback_callbacks
|
669
|
+
actions_to_commit = @actions.reject(&:aborted?).reject(&:skipped?)
|
670
|
+
actions_to_commit.each(&:on_rollback)
|
671
|
+
end
|
672
|
+
end
|
673
|
+
end
|