dynamoid 3.10.0 → 3.11.0
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 +14 -0
- data/README.md +182 -2
- 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 +9 -5
- data/lib/dynamoid/components.rb +1 -0
- data/lib/dynamoid/config.rb +2 -0
- data/lib/dynamoid/criteria/chain.rb +63 -18
- data/lib/dynamoid/criteria/where_conditions.rb +13 -6
- data/lib/dynamoid/dirty.rb +86 -11
- data/lib/dynamoid/dumping.rb +36 -14
- data/lib/dynamoid/errors.rb +14 -2
- data/lib/dynamoid/finders.rb +6 -6
- data/lib/dynamoid/loadable.rb +1 -0
- data/lib/dynamoid/persistence/inc.rb +6 -7
- 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 +5 -2
- data/lib/dynamoid/persistence/update_fields.rb +5 -3
- data/lib/dynamoid/persistence/upsert.rb +5 -4
- data/lib/dynamoid/persistence.rb +38 -17
- 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 +60 -0
- data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +59 -0
- data/lib/dynamoid/transaction_write/destroy.rb +79 -0
- data/lib/dynamoid/transaction_write/save.rb +164 -0
- data/lib/dynamoid/transaction_write/update_attributes.rb +46 -0
- data/lib/dynamoid/transaction_write/update_fields.rb +102 -0
- data/lib/dynamoid/transaction_write/upsert.rb +96 -0
- data/lib/dynamoid/transaction_write.rb +464 -0
- data/lib/dynamoid/type_casting.rb +3 -1
- data/lib/dynamoid/undumping.rb +13 -2
- data/lib/dynamoid/validations.rb +1 -1
- data/lib/dynamoid/version.rb +1 -1
- data/lib/dynamoid.rb +7 -0
- metadata +18 -5
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require 'dynamoid/persistence/update_validations'
|
5
|
+
|
6
|
+
module Dynamoid
|
7
|
+
class TransactionWrite
|
8
|
+
class UpdateFields < Base
|
9
|
+
def initialize(model_class, hash_key, range_key, attributes)
|
10
|
+
super()
|
11
|
+
|
12
|
+
@model_class = model_class
|
13
|
+
@hash_key = hash_key
|
14
|
+
@range_key = range_key
|
15
|
+
@attributes = attributes
|
16
|
+
end
|
17
|
+
|
18
|
+
def on_registration
|
19
|
+
validate_primary_key!
|
20
|
+
Dynamoid::Persistence::UpdateValidations.validate_attributes_exist(@model_class, @attributes)
|
21
|
+
end
|
22
|
+
|
23
|
+
def on_commit; end
|
24
|
+
|
25
|
+
def on_rollback; end
|
26
|
+
|
27
|
+
def aborted?
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
def skipped?
|
32
|
+
@attributes.empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
def observable_by_user_result
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def action_request
|
40
|
+
# changed attributes to persist
|
41
|
+
changes = @attributes.dup
|
42
|
+
changes = add_timestamps(changes, skip_created_at: true)
|
43
|
+
changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
|
44
|
+
|
45
|
+
# primary key to look up an item to update
|
46
|
+
key = { @model_class.hash_key => @hash_key }
|
47
|
+
key[@model_class.range_key] = @range_key if @model_class.range_key?
|
48
|
+
|
49
|
+
# Build UpdateExpression and keep names and values placeholders mapping
|
50
|
+
# in ExpressionAttributeNames and ExpressionAttributeValues.
|
51
|
+
update_expression_statements = []
|
52
|
+
expression_attribute_names = {}
|
53
|
+
expression_attribute_values = {}
|
54
|
+
|
55
|
+
changes_dumped.each_with_index do |(name, value), i|
|
56
|
+
name_placeholder = "#_n#{i}"
|
57
|
+
value_placeholder = ":_s#{i}"
|
58
|
+
|
59
|
+
update_expression_statements << "#{name_placeholder} = #{value_placeholder}"
|
60
|
+
expression_attribute_names[name_placeholder] = name
|
61
|
+
expression_attribute_values[value_placeholder] = value
|
62
|
+
end
|
63
|
+
|
64
|
+
update_expression = "SET #{update_expression_statements.join(', ')}"
|
65
|
+
|
66
|
+
# require primary key to exist
|
67
|
+
condition_expression = "attribute_exists(#{@model_class.hash_key})"
|
68
|
+
if @model_class.range_key?
|
69
|
+
condition_expression += " AND attribute_exists(#{@model_class.range_key})"
|
70
|
+
end
|
71
|
+
|
72
|
+
{
|
73
|
+
update: {
|
74
|
+
key: key,
|
75
|
+
table_name: @model_class.table_name,
|
76
|
+
update_expression: update_expression,
|
77
|
+
expression_attribute_names: expression_attribute_names,
|
78
|
+
expression_attribute_values: expression_attribute_values,
|
79
|
+
condition_expression: condition_expression
|
80
|
+
}
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def validate_primary_key!
|
87
|
+
raise Dynamoid::Errors::MissingHashKey if @hash_key.nil?
|
88
|
+
raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @range_key.nil?
|
89
|
+
end
|
90
|
+
|
91
|
+
def add_timestamps(attributes, skip_created_at: false)
|
92
|
+
return attributes unless @model_class.timestamps_enabled?
|
93
|
+
|
94
|
+
result = attributes.clone
|
95
|
+
timestamp = DateTime.now.in_time_zone(Time.zone)
|
96
|
+
result[:created_at] ||= timestamp unless skip_created_at
|
97
|
+
result[:updated_at] ||= timestamp
|
98
|
+
result
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require 'dynamoid/persistence/update_validations'
|
5
|
+
|
6
|
+
module Dynamoid
|
7
|
+
class TransactionWrite
|
8
|
+
class Upsert < Base
|
9
|
+
def initialize(model_class, hash_key, range_key, attributes)
|
10
|
+
super()
|
11
|
+
|
12
|
+
@model_class = model_class
|
13
|
+
@hash_key = hash_key
|
14
|
+
@range_key = range_key
|
15
|
+
@attributes = attributes
|
16
|
+
end
|
17
|
+
|
18
|
+
def on_registration
|
19
|
+
validate_primary_key!
|
20
|
+
Dynamoid::Persistence::UpdateValidations.validate_attributes_exist(@model_class, @attributes)
|
21
|
+
end
|
22
|
+
|
23
|
+
def on_commit; end
|
24
|
+
|
25
|
+
def on_rollback; end
|
26
|
+
|
27
|
+
def aborted?
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
def skipped?
|
32
|
+
attributes_to_assign = @attributes.except(@model_class.hash_key, @model_class.range_key)
|
33
|
+
attributes_to_assign.empty? && !@model_class.timestamps_enabled?
|
34
|
+
end
|
35
|
+
|
36
|
+
def observable_by_user_result
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def action_request
|
41
|
+
# changed attributes to persist
|
42
|
+
changes = @attributes.dup
|
43
|
+
changes = add_timestamps(changes, skip_created_at: true)
|
44
|
+
changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
|
45
|
+
|
46
|
+
# primary key to look up an item to update
|
47
|
+
key = { @model_class.hash_key => @hash_key }
|
48
|
+
key[@model_class.range_key] = @range_key if @model_class.range_key?
|
49
|
+
|
50
|
+
# Build UpdateExpression and keep names and values placeholders mapping
|
51
|
+
# in ExpressionAttributeNames and ExpressionAttributeValues.
|
52
|
+
update_expression_statements = []
|
53
|
+
expression_attribute_names = {}
|
54
|
+
expression_attribute_values = {}
|
55
|
+
|
56
|
+
changes_dumped.each_with_index do |(name, value), i|
|
57
|
+
name_placeholder = "#_n#{i}"
|
58
|
+
value_placeholder = ":_s#{i}"
|
59
|
+
|
60
|
+
update_expression_statements << "#{name_placeholder} = #{value_placeholder}"
|
61
|
+
expression_attribute_names[name_placeholder] = name
|
62
|
+
expression_attribute_values[value_placeholder] = value
|
63
|
+
end
|
64
|
+
|
65
|
+
update_expression = "SET #{update_expression_statements.join(', ')}"
|
66
|
+
|
67
|
+
{
|
68
|
+
update: {
|
69
|
+
key: key,
|
70
|
+
table_name: @model_class.table_name,
|
71
|
+
update_expression: update_expression,
|
72
|
+
expression_attribute_names: expression_attribute_names,
|
73
|
+
expression_attribute_values: expression_attribute_values
|
74
|
+
}
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def validate_primary_key!
|
81
|
+
raise Dynamoid::Errors::MissingHashKey if @hash_key.nil?
|
82
|
+
raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @range_key.nil?
|
83
|
+
end
|
84
|
+
|
85
|
+
def add_timestamps(attributes, skip_created_at: false)
|
86
|
+
return attributes unless @model_class.timestamps_enabled?
|
87
|
+
|
88
|
+
result = attributes.clone
|
89
|
+
timestamp = DateTime.now.in_time_zone(Time.zone)
|
90
|
+
result[:created_at] ||= timestamp unless skip_created_at
|
91
|
+
result[:updated_at] ||= timestamp
|
92
|
+
result
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,464 @@
|
|
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
|
+
|
12
|
+
module Dynamoid
|
13
|
+
# The class +TransactionWrite+ provides means to perform multiple modifying
|
14
|
+
# operations in transaction, that is atomically, so that either all of them
|
15
|
+
# succeed, or all of them fail.
|
16
|
+
#
|
17
|
+
# The persisting methods are supposed to be as close as possible to their
|
18
|
+
# non-transactional counterparts like +.create+, +#save+ and +#delete+:
|
19
|
+
#
|
20
|
+
# user = User.new()
|
21
|
+
# payment = Payment.find(1)
|
22
|
+
#
|
23
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
24
|
+
# t.save! user
|
25
|
+
# t.create! Account, name: 'A'
|
26
|
+
# t.delete payment
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# The only difference is that the methods are called on a transaction
|
30
|
+
# instance and a model or a model class should be specified.
|
31
|
+
#
|
32
|
+
# So +user.save!+ becomes +t.save!(user)+, +Account.create!(name: 'A')+
|
33
|
+
# becomes +t.create!(Account, name: 'A')+, and +payment.delete+ becomes
|
34
|
+
# +t.delete(payment)+.
|
35
|
+
#
|
36
|
+
# A transaction can be used without a block. This way a transaction instance
|
37
|
+
# should be instantiated and committed manually with +#commit+ method:
|
38
|
+
#
|
39
|
+
# t = Dynamoid::TransactionWrite.new
|
40
|
+
#
|
41
|
+
# t.save! user
|
42
|
+
# t.create! Account, name: 'A'
|
43
|
+
# t.delete payment
|
44
|
+
#
|
45
|
+
# t.commit
|
46
|
+
#
|
47
|
+
# Some persisting methods are intentionally not available in a transaction,
|
48
|
+
# e.g. +.update+ and +.update!+ that simply call +.find+ and
|
49
|
+
# +#update_attributes+ methods. These methods perform multiple operations so
|
50
|
+
# cannot be implemented in a transactional atomic way.
|
51
|
+
#
|
52
|
+
#
|
53
|
+
# ### DynamoDB's transactions
|
54
|
+
#
|
55
|
+
# The main difference between DynamoDB transactions and a common interface is
|
56
|
+
# that DynamoDB's transactions are executed in batch. So in Dynamoid no
|
57
|
+
# changes are actually persisted when some transactional method (e.g+ `#save+) is
|
58
|
+
# called. All the changes are persisted at the end.
|
59
|
+
#
|
60
|
+
# A +TransactWriteItems+ DynamoDB operation is used (see
|
61
|
+
# [documentation](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html)
|
62
|
+
# for details).
|
63
|
+
#
|
64
|
+
#
|
65
|
+
# ### Callbacks
|
66
|
+
#
|
67
|
+
# The transactional methods support +before_+, +after_+ and +around_+
|
68
|
+
# callbacks to the extend the non-transactional methods support them.
|
69
|
+
#
|
70
|
+
# There is important difference - a transactional method runs callbacks
|
71
|
+
# immediately (even +after_+ ones) when it is called before changes are
|
72
|
+
# actually persisted. So code in +after_+ callbacks does not see observes
|
73
|
+
# them in DynamoDB and so for.
|
74
|
+
#
|
75
|
+
# When a callback aborts persisting of a model or a model is invalid then
|
76
|
+
# transaction is not aborted and may commit successfully.
|
77
|
+
#
|
78
|
+
#
|
79
|
+
# ### Transaction rollback
|
80
|
+
#
|
81
|
+
# A transaction is rolled back on DynamoDB's side automatically when:
|
82
|
+
# - an ongoing operation is in the process of updating the same item.
|
83
|
+
# - there is insufficient provisioned capacity for the transaction to be completed.
|
84
|
+
# - 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.
|
85
|
+
# - the aggregate size of the items in the transaction exceeds 4 MB.
|
86
|
+
# - there is a user error, such as an invalid data format.
|
87
|
+
#
|
88
|
+
# A transaction can be interrupted simply by an exception raised within a
|
89
|
+
# block. As far as no changes are actually persisted before the +#commit+
|
90
|
+
# method call - there is nothing to undo on the DynamoDB's site.
|
91
|
+
#
|
92
|
+
# Raising +Dynamoid::Errors::Rollback+ exception leads to interrupting a
|
93
|
+
# transation and it isn't propogated:
|
94
|
+
#
|
95
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
96
|
+
# t.save! user
|
97
|
+
# t.create! Account, name: 'A'
|
98
|
+
#
|
99
|
+
# if user.is_admin?
|
100
|
+
# raise Dynamoid::Errors::Rollback
|
101
|
+
# end
|
102
|
+
# end
|
103
|
+
#
|
104
|
+
# When a transaction is successfully committed or rolled backed -
|
105
|
+
# corresponding +#after_commit+ or +#after_rollback+ callbacks are run for
|
106
|
+
# each involved model.
|
107
|
+
class TransactionWrite
|
108
|
+
def self.execute
|
109
|
+
transaction = new
|
110
|
+
|
111
|
+
begin
|
112
|
+
yield transaction
|
113
|
+
rescue StandardError => e
|
114
|
+
transaction.rollback
|
115
|
+
|
116
|
+
unless e.is_a?(Dynamoid::Errors::Rollback)
|
117
|
+
raise e
|
118
|
+
end
|
119
|
+
else
|
120
|
+
transaction.commit
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def initialize
|
125
|
+
@actions = []
|
126
|
+
end
|
127
|
+
|
128
|
+
# Persist all the changes.
|
129
|
+
#
|
130
|
+
# transaction = Dynamoid::TransactionWrite.new
|
131
|
+
# # ...
|
132
|
+
# transaction.commit
|
133
|
+
def commit
|
134
|
+
actions_to_commit = @actions.reject(&:aborted?).reject(&:skipped?)
|
135
|
+
return if actions_to_commit.empty?
|
136
|
+
|
137
|
+
action_requests = actions_to_commit.map(&:action_request)
|
138
|
+
Dynamoid.adapter.transact_write_items(action_requests)
|
139
|
+
actions_to_commit.each(&:on_commit)
|
140
|
+
|
141
|
+
nil
|
142
|
+
rescue Aws::Errors::ServiceError
|
143
|
+
run_on_rollback_callbacks
|
144
|
+
raise
|
145
|
+
end
|
146
|
+
|
147
|
+
def rollback
|
148
|
+
run_on_rollback_callbacks
|
149
|
+
end
|
150
|
+
|
151
|
+
# Create new model or persist changes in already existing one.
|
152
|
+
#
|
153
|
+
# Run the validation and callbacks. Returns +true+ if saving is successful
|
154
|
+
# and +false+ otherwise.
|
155
|
+
#
|
156
|
+
# user = User.new
|
157
|
+
#
|
158
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
159
|
+
# t.save!(user)
|
160
|
+
# end
|
161
|
+
#
|
162
|
+
# Validation can be skipped with +validate: false+ option:
|
163
|
+
#
|
164
|
+
# user = User.new(age: -1)
|
165
|
+
#
|
166
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
167
|
+
# t.save!(user, validate: false)
|
168
|
+
# end
|
169
|
+
#
|
170
|
+
# +save!+ by default sets timestamps attributes - +created_at+ and
|
171
|
+
# +updated_at+ when creates new model and updates +updated_at+ attribute
|
172
|
+
# when updates already existing one.
|
173
|
+
#
|
174
|
+
# If a model is new and hash key (+id+ by default) is not assigned yet
|
175
|
+
# it was assigned implicitly with random UUID value.
|
176
|
+
#
|
177
|
+
# When a model is not persisted - its id should have unique value.
|
178
|
+
# Otherwise a transaction will be rolled back.
|
179
|
+
#
|
180
|
+
# @param model [Dynamoid::Document] a model
|
181
|
+
# @param options [Hash] (optional)
|
182
|
+
# @option options [true|false] :validate validate a model or not - +true+ by default (optional)
|
183
|
+
# @return [true|false] Whether saving successful or not
|
184
|
+
def save!(model, **options)
|
185
|
+
action = Dynamoid::TransactionWrite::Save.new(model, **options, raise_error: true)
|
186
|
+
register_action action
|
187
|
+
end
|
188
|
+
|
189
|
+
# Create new model or persist changes in already existing one.
|
190
|
+
#
|
191
|
+
# Run the validation and callbacks. Raise
|
192
|
+
# +Dynamoid::Errors::DocumentNotValid+ unless this object is valid.
|
193
|
+
#
|
194
|
+
# user = User.new
|
195
|
+
#
|
196
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
197
|
+
# t.save(user)
|
198
|
+
# end
|
199
|
+
#
|
200
|
+
# Validation can be skipped with +validate: false+ option:
|
201
|
+
#
|
202
|
+
# user = User.new(age: -1)
|
203
|
+
#
|
204
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
205
|
+
# t.save(user, validate: false)
|
206
|
+
# end
|
207
|
+
#
|
208
|
+
# +save+ by default sets timestamps attributes - +created_at+ and
|
209
|
+
# +updated_at+ when creates new model and updates +updated_at+ attribute
|
210
|
+
# when updates already existing one.
|
211
|
+
#
|
212
|
+
# If a model is new and hash key (+id+ by default) is not assigned yet
|
213
|
+
# it was assigned implicitly with random UUID value.
|
214
|
+
#
|
215
|
+
# When a model is not persisted - its id should have unique value.
|
216
|
+
# Otherwise a transaction will be rolled back.
|
217
|
+
#
|
218
|
+
# @param model [Dynamoid::Document] a model
|
219
|
+
# @param options [Hash] (optional)
|
220
|
+
# @option options [true|false] :validate validate a model or not - +true+ by default (optional)
|
221
|
+
# @return [true|false] Whether saving successful or not
|
222
|
+
def save(model, **options)
|
223
|
+
action = Dynamoid::TransactionWrite::Save.new(model, **options, raise_error: false)
|
224
|
+
register_action action
|
225
|
+
end
|
226
|
+
|
227
|
+
# Create a model.
|
228
|
+
#
|
229
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
230
|
+
# t.create!(User, name: 'A')
|
231
|
+
# end
|
232
|
+
#
|
233
|
+
# Accepts both Hash and Array of Hashes and can create several models.
|
234
|
+
#
|
235
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
236
|
+
# t.create!(User, [{name: 'A'}, {name: 'B'}, {name: 'C'}])
|
237
|
+
# end
|
238
|
+
#
|
239
|
+
# Instantiates a model and pass it into an optional block to set other
|
240
|
+
# attributes.
|
241
|
+
#
|
242
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
243
|
+
# t.create!(User, name: 'A') do |user|
|
244
|
+
# user.initialize_roles
|
245
|
+
# end
|
246
|
+
# end
|
247
|
+
#
|
248
|
+
# Validates model and runs callbacks.
|
249
|
+
#
|
250
|
+
# @param model_class [Class] a model class which should be instantiated
|
251
|
+
# @param attributes [Hash|Array<Hash>] attributes of a model
|
252
|
+
# @param block [Proc] a block to process a model after initialization
|
253
|
+
# @return [Dynamoid::Document] a model that was instantiated but not yet persisted
|
254
|
+
def create!(model_class, attributes = {}, &block)
|
255
|
+
if attributes.is_a? Array
|
256
|
+
attributes.map do |attr|
|
257
|
+
action = Dynamoid::TransactionWrite::Create.new(model_class, attr, raise_error: true, &block)
|
258
|
+
register_action action
|
259
|
+
end
|
260
|
+
else
|
261
|
+
action = Dynamoid::TransactionWrite::Create.new(model_class, attributes, raise_error: true, &block)
|
262
|
+
register_action action
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Create a model.
|
267
|
+
#
|
268
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
269
|
+
# t.create(User, name: 'A')
|
270
|
+
# end
|
271
|
+
#
|
272
|
+
# Accepts both Hash and Array of Hashes and can create several models.
|
273
|
+
#
|
274
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
275
|
+
# t.create(User, [{name: 'A'}, {name: 'B'}, {name: 'C'}])
|
276
|
+
# end
|
277
|
+
#
|
278
|
+
# Instantiates a model and pass it into an optional block to set other
|
279
|
+
# attributes.
|
280
|
+
#
|
281
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
282
|
+
# t.create(User, name: 'A') do |user|
|
283
|
+
# user.initialize_roles
|
284
|
+
# end
|
285
|
+
# end
|
286
|
+
#
|
287
|
+
# Validates model and runs callbacks.
|
288
|
+
#
|
289
|
+
# @param model_class [Class] a model class which should be instantiated
|
290
|
+
# @param attributes [Hash|Array<Hash>] attributes of a model
|
291
|
+
# @param block [Proc] a block to process a model after initialization
|
292
|
+
# @return [Dynamoid::Document] a model that was instantiated but not yet persisted
|
293
|
+
def create(model_class, attributes = {}, &block)
|
294
|
+
if attributes.is_a? Array
|
295
|
+
attributes.map do |attr|
|
296
|
+
action = Dynamoid::TransactionWrite::Create.new(model_class, attr, raise_error: false, &block)
|
297
|
+
register_action action
|
298
|
+
end
|
299
|
+
else
|
300
|
+
action = Dynamoid::TransactionWrite::Create.new(model_class, attributes, raise_error: false, &block)
|
301
|
+
register_action action
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# Update an existing document or create a new one.
|
306
|
+
#
|
307
|
+
# If a document with specified hash and range keys doesn't exist it
|
308
|
+
# creates a new document with specified attributes. Doesn't run
|
309
|
+
# validations and callbacks.
|
310
|
+
#
|
311
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
312
|
+
# t.upsert(User, '1', age: 26)
|
313
|
+
# end
|
314
|
+
#
|
315
|
+
# If range key is declared for a model it should be passed as well:
|
316
|
+
#
|
317
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
318
|
+
# t.upsert(User, '1', 'Tylor', age: 26)
|
319
|
+
# end
|
320
|
+
#
|
321
|
+
# Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
|
322
|
+
# attributes is not declared in the model class.
|
323
|
+
#
|
324
|
+
# @param model_class [Class] a model class
|
325
|
+
# @param hash_key [Scalar value] hash key value
|
326
|
+
# @param range_key [Scalar value] range key value (optional)
|
327
|
+
# @param attributes [Hash]
|
328
|
+
# @return [nil]
|
329
|
+
def upsert(model_class, hash_key, range_key = nil, attributes) # rubocop:disable Style/OptionalArguments
|
330
|
+
action = Dynamoid::TransactionWrite::Upsert.new(model_class, hash_key, range_key, attributes)
|
331
|
+
register_action action
|
332
|
+
end
|
333
|
+
|
334
|
+
# Update document.
|
335
|
+
#
|
336
|
+
# Doesn't run validations and callbacks.
|
337
|
+
#
|
338
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
339
|
+
# t.update_fields(User, '1', age: 26)
|
340
|
+
# end
|
341
|
+
#
|
342
|
+
# If range key is declared for a model it should be passed as well:
|
343
|
+
#
|
344
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
345
|
+
# t.update_fields(User, '1', 'Tylor', age: 26)
|
346
|
+
# end
|
347
|
+
#
|
348
|
+
# Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
|
349
|
+
# attributes is not declared in the model class.
|
350
|
+
#
|
351
|
+
# @param model_class [Class] a model class
|
352
|
+
# @param hash_key [Scalar value] hash key value
|
353
|
+
# @param range_key [Scalar value] range key value (optional)
|
354
|
+
# @param attributes [Hash]
|
355
|
+
# @return [nil]
|
356
|
+
def update_fields(model_class, hash_key, range_key = nil, attributes) # rubocop:disable Style/OptionalArguments
|
357
|
+
action = Dynamoid::TransactionWrite::UpdateFields.new(model_class, hash_key, range_key, attributes)
|
358
|
+
register_action action
|
359
|
+
end
|
360
|
+
|
361
|
+
# Update multiple attributes at once.
|
362
|
+
#
|
363
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
364
|
+
# t.update_attributes(user, age: 27, last_name: 'Tylor')
|
365
|
+
# end
|
366
|
+
#
|
367
|
+
# Returns +true+ if saving is successful and +false+
|
368
|
+
# otherwise.
|
369
|
+
#
|
370
|
+
# @param model [Dynamoid::Document] a model
|
371
|
+
# @param attributes [Hash] a hash of attributes to update
|
372
|
+
# @return [true|false] Whether updating successful or not
|
373
|
+
def update_attributes(model, attributes)
|
374
|
+
action = Dynamoid::TransactionWrite::UpdateAttributes.new(model, attributes, raise_error: false)
|
375
|
+
register_action action
|
376
|
+
end
|
377
|
+
|
378
|
+
# Update multiple attributes at once.
|
379
|
+
#
|
380
|
+
# Returns +true+ if saving is successful and +false+
|
381
|
+
# otherwise.
|
382
|
+
#
|
383
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
384
|
+
# t.update_attributes(user, age: 27, last_name: 'Tylor')
|
385
|
+
# end
|
386
|
+
#
|
387
|
+
# Raises a +Dynamoid::Errors::DocumentNotValid+ exception if some vaidation
|
388
|
+
# fails.
|
389
|
+
#
|
390
|
+
# @param model [Dynamoid::Document] a model
|
391
|
+
# @param attributes [Hash] a hash of attributes to update
|
392
|
+
def update_attributes!(model, attributes)
|
393
|
+
action = Dynamoid::TransactionWrite::UpdateAttributes.new(model, attributes, raise_error: true)
|
394
|
+
register_action action
|
395
|
+
end
|
396
|
+
|
397
|
+
# Delete a model.
|
398
|
+
#
|
399
|
+
# Can be called either with a model:
|
400
|
+
#
|
401
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
402
|
+
# t.delete(user)
|
403
|
+
# end
|
404
|
+
#
|
405
|
+
# or with a primary key:
|
406
|
+
#
|
407
|
+
# Dynamoid::TransactionWrite.execute do |t|
|
408
|
+
# t.delete(User, user_id)
|
409
|
+
# end
|
410
|
+
#
|
411
|
+
# Raise +MissingRangeKey+ if a range key is declared but not passed as argument.
|
412
|
+
#
|
413
|
+
# @param model_or_model_class [Class|Dynamoid::Document] either model or model class
|
414
|
+
# @param hash_key [Scalar value] hash key value
|
415
|
+
# @param range_key [Scalar value] range key value (optional)
|
416
|
+
# @return [Dynamoid::Document] self
|
417
|
+
def delete(model_or_model_class, hash_key = nil, range_key = nil)
|
418
|
+
action = if model_or_model_class.is_a? Class
|
419
|
+
Dynamoid::TransactionWrite::DeleteWithPrimaryKey.new(model_or_model_class, hash_key, range_key)
|
420
|
+
else
|
421
|
+
Dynamoid::TransactionWrite::DeleteWithInstance.new(model_or_model_class)
|
422
|
+
end
|
423
|
+
register_action action
|
424
|
+
end
|
425
|
+
|
426
|
+
# Delete a model.
|
427
|
+
#
|
428
|
+
# Runs callbacks.
|
429
|
+
#
|
430
|
+
# Raises +Dynamoid::Errors::RecordNotDestroyed+ exception if model deleting
|
431
|
+
# failed (e.g. aborted by a callback).
|
432
|
+
#
|
433
|
+
# @param model [Dynamoid::Document] a model
|
434
|
+
# @return [Dynamoid::Document|false] returns self if destoying is succefull, +false+ otherwise
|
435
|
+
def destroy!(model)
|
436
|
+
action = Dynamoid::TransactionWrite::Destroy.new(model, raise_error: true)
|
437
|
+
register_action action
|
438
|
+
end
|
439
|
+
|
440
|
+
# Delete a model.
|
441
|
+
#
|
442
|
+
# Runs callbacks.
|
443
|
+
#
|
444
|
+
# @param model [Dynamoid::Document] a model
|
445
|
+
# @return [Dynamoid::Document] self
|
446
|
+
def destroy(model)
|
447
|
+
action = Dynamoid::TransactionWrite::Destroy.new(model, raise_error: false)
|
448
|
+
register_action action
|
449
|
+
end
|
450
|
+
|
451
|
+
private
|
452
|
+
|
453
|
+
def register_action(action)
|
454
|
+
@actions << action
|
455
|
+
action.on_registration
|
456
|
+
action.observable_by_user_result
|
457
|
+
end
|
458
|
+
|
459
|
+
def run_on_rollback_callbacks
|
460
|
+
actions_to_commit = @actions.reject(&:aborted?).reject(&:skipped?)
|
461
|
+
actions_to_commit.each(&:on_rollback)
|
462
|
+
end
|
463
|
+
end
|
464
|
+
end
|
data/lib/dynamoid/undumping.rb
CHANGED
@@ -238,7 +238,8 @@ module Dynamoid
|
|
238
238
|
class SerializedUndumper < Base
|
239
239
|
# We must use YAML.safe_load in Ruby 3.1 to handle serialized Set class
|
240
240
|
minimum_ruby_version = ->(version) { Gem::Version.new(RUBY_VERSION) >= Gem::Version.new(version) }
|
241
|
-
|
241
|
+
|
242
|
+
# Once we drop support for Rubies older than 2.6 we can remove this condition (with major version bump)!
|
242
243
|
# YAML_SAFE_LOAD = minimum_ruby_version.call("2.6")
|
243
244
|
# But we don't want to change behavior for Ruby <= 3.0 that has been using the gem, without a major version bump
|
244
245
|
YAML_SAFE_LOAD = minimum_ruby_version.call('3.1')
|
@@ -284,7 +285,17 @@ module Dynamoid
|
|
284
285
|
|
285
286
|
class BinaryUndumper < Base
|
286
287
|
def process(value)
|
287
|
-
|
288
|
+
store_as_binary = if @options[:store_as_native_binary].nil?
|
289
|
+
Dynamoid.config.store_binary_as_native
|
290
|
+
else
|
291
|
+
@options[:store_as_native_binary]
|
292
|
+
end
|
293
|
+
|
294
|
+
if store_as_binary
|
295
|
+
value.string # expect StringIO here
|
296
|
+
else
|
297
|
+
Base64.strict_decode64(value)
|
298
|
+
end
|
288
299
|
end
|
289
300
|
end
|
290
301
|
|