dynamoid 3.3.0 → 3.7.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 +104 -1
- data/README.md +146 -52
- data/lib/dynamoid.rb +1 -0
- data/lib/dynamoid/adapter.rb +20 -7
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +70 -37
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +3 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +20 -12
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +5 -4
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/backoff.rb +2 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +2 -3
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/start_key.rb +2 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +4 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +4 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +1 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +2 -1
- data/lib/dynamoid/application_time_zone.rb +1 -0
- data/lib/dynamoid/associations.rb +182 -19
- data/lib/dynamoid/associations/association.rb +10 -2
- data/lib/dynamoid/associations/belongs_to.rb +2 -1
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +2 -1
- data/lib/dynamoid/associations/has_many.rb +2 -1
- data/lib/dynamoid/associations/has_one.rb +2 -1
- data/lib/dynamoid/associations/many_association.rb +68 -23
- data/lib/dynamoid/associations/single_association.rb +31 -4
- data/lib/dynamoid/components.rb +2 -0
- data/lib/dynamoid/config.rb +15 -3
- data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +1 -0
- data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +1 -0
- data/lib/dynamoid/config/options.rb +1 -0
- data/lib/dynamoid/criteria.rb +9 -1
- data/lib/dynamoid/criteria/chain.rb +421 -46
- data/lib/dynamoid/criteria/ignored_conditions_detector.rb +3 -3
- data/lib/dynamoid/criteria/key_fields_detector.rb +31 -10
- data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +3 -2
- data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +1 -1
- data/lib/dynamoid/dirty.rb +119 -64
- data/lib/dynamoid/document.rb +133 -46
- data/lib/dynamoid/dumping.rb +9 -0
- data/lib/dynamoid/dynamodb_time_zone.rb +1 -0
- data/lib/dynamoid/errors.rb +2 -0
- data/lib/dynamoid/fields.rb +251 -39
- data/lib/dynamoid/fields/declare.rb +86 -0
- data/lib/dynamoid/finders.rb +69 -32
- data/lib/dynamoid/identity_map.rb +6 -0
- data/lib/dynamoid/indexes.rb +86 -17
- data/lib/dynamoid/loadable.rb +2 -2
- data/lib/dynamoid/log/formatter.rb +26 -0
- data/lib/dynamoid/middleware/identity_map.rb +1 -0
- data/lib/dynamoid/persistence.rb +502 -104
- data/lib/dynamoid/persistence/import.rb +2 -1
- data/lib/dynamoid/persistence/save.rb +1 -0
- data/lib/dynamoid/persistence/update_fields.rb +5 -2
- data/lib/dynamoid/persistence/update_validations.rb +18 -0
- data/lib/dynamoid/persistence/upsert.rb +5 -3
- data/lib/dynamoid/primary_key_type_mapping.rb +1 -0
- data/lib/dynamoid/railtie.rb +1 -0
- data/lib/dynamoid/tasks.rb +3 -1
- data/lib/dynamoid/tasks/database.rb +1 -0
- data/lib/dynamoid/type_casting.rb +12 -2
- data/lib/dynamoid/undumping.rb +8 -0
- data/lib/dynamoid/validations.rb +6 -1
- data/lib/dynamoid/version.rb +1 -1
- metadata +48 -75
- data/.coveralls.yml +0 -1
- data/.document +0 -5
- data/.gitignore +0 -74
- data/.rspec +0 -2
- data/.rubocop.yml +0 -71
- data/.rubocop_todo.yml +0 -55
- data/.travis.yml +0 -44
- data/Appraisals +0 -22
- data/Gemfile +0 -8
- data/Rakefile +0 -46
- data/Vagrantfile +0 -29
- data/docker-compose.yml +0 -7
- data/dynamoid.gemspec +0 -57
- data/gemfiles/rails_4_2.gemfile +0 -9
- data/gemfiles/rails_5_0.gemfile +0 -8
- data/gemfiles/rails_5_1.gemfile +0 -8
- data/gemfiles/rails_5_2.gemfile +0 -8
- data/gemfiles/rails_6_0.gemfile +0 -8
data/lib/dynamoid/loadable.rb
CHANGED
@@ -10,7 +10,7 @@ module Dynamoid
|
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
|
-
# Reload an object from the database -- if you suspect the object has changed in the
|
13
|
+
# Reload an object from the database -- if you suspect the object has changed in the data store and you need those
|
14
14
|
# changes to be reflected immediately, you would call this method. This is a consistent read.
|
15
15
|
#
|
16
16
|
# @return [Dynamoid::Document] the document this method was called on
|
@@ -23,7 +23,7 @@ module Dynamoid
|
|
23
23
|
options[:range_key] = range_value
|
24
24
|
end
|
25
25
|
|
26
|
-
self.attributes = self.class.find(hash_key, options).attributes
|
26
|
+
self.attributes = self.class.find(hash_key, **options).attributes
|
27
27
|
@associations.values.each(&:reset)
|
28
28
|
self
|
29
29
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Dynamoid
|
2
|
+
module Log
|
3
|
+
module Formatter
|
4
|
+
|
5
|
+
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/Log/Formatter.html
|
6
|
+
# https://docs.aws.amazon.com/sdk-for-ruby/v2/api/Seahorse/Client/Response.html
|
7
|
+
# https://aws.amazon.com/ru/blogs/developer/logging-requests/
|
8
|
+
class Debug
|
9
|
+
def format(response)
|
10
|
+
bold = "\x1b[1m"
|
11
|
+
color = "\x1b[34m"
|
12
|
+
reset = "\x1b[0m"
|
13
|
+
|
14
|
+
[
|
15
|
+
response.context.operation.name,
|
16
|
+
"#{bold}#{color}\nRequest:\n#{reset}#{bold}",
|
17
|
+
JSON.pretty_generate(JSON.parse(response.context.http_request.body.string)),
|
18
|
+
"#{bold}#{color}\nResponse:\n#{reset}#{bold}",
|
19
|
+
JSON.pretty_generate(JSON.parse(response.context.http_response.body.string)),
|
20
|
+
reset
|
21
|
+
].join("\n")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/dynamoid/persistence.rb
CHANGED
@@ -8,10 +8,11 @@ require 'dynamoid/persistence/import'
|
|
8
8
|
require 'dynamoid/persistence/update_fields'
|
9
9
|
require 'dynamoid/persistence/upsert'
|
10
10
|
require 'dynamoid/persistence/save'
|
11
|
+
require 'dynamoid/persistence/update_validations'
|
11
12
|
|
12
13
|
# encoding: utf-8
|
13
14
|
module Dynamoid
|
14
|
-
# Persistence is responsible for dumping objects to and marshalling objects from the
|
15
|
+
# Persistence is responsible for dumping objects to and marshalling objects from the data store. It tries to reserialize
|
15
16
|
# values to be of the same type as when they were passed in, based on the fields in the class.
|
16
17
|
module Persistence
|
17
18
|
extend ActiveSupport::Concern
|
@@ -19,6 +20,7 @@ module Dynamoid
|
|
19
20
|
attr_accessor :new_record
|
20
21
|
alias new_record? new_record
|
21
22
|
|
23
|
+
# @private
|
22
24
|
UNIX_EPOCH_DATE = Date.new(1970, 1, 1).freeze
|
23
25
|
|
24
26
|
module ClassMethods
|
@@ -30,13 +32,64 @@ module Dynamoid
|
|
30
32
|
|
31
33
|
# Create a table.
|
32
34
|
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
35
|
+
# Uses a configuration specified in a model class (with the +table+
|
36
|
+
# method) e.g. table name, schema (hash and range keys), global and local
|
37
|
+
# secondary indexes, billing mode and write/read capacity.
|
38
|
+
#
|
39
|
+
# For instance here
|
40
|
+
#
|
41
|
+
# class User
|
42
|
+
# include Dynamoid::Document
|
43
|
+
#
|
44
|
+
# table key: :uuid
|
45
|
+
# range :last_name
|
46
|
+
#
|
47
|
+
# field :first_name
|
48
|
+
# field :last_name
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# User.create_table
|
52
|
+
#
|
53
|
+
# +create_table+ method call will create a table +dynamoid_users+ with
|
54
|
+
# hash key +uuid+ and range key +name+, DynamoDB default billing mode and
|
55
|
+
# Dynamoid default read/write capacity units (100/20).
|
56
|
+
#
|
57
|
+
# All the configuration can be overridden with +options+ argument.
|
58
|
+
#
|
59
|
+
# User.create_table(table_name: 'users', read_capacity: 200, write_capacity: 40)
|
60
|
+
#
|
61
|
+
# Dynamoid creates a table synchronously by default. DynamoDB table
|
62
|
+
# creation is an asynchronous operation and a client should wait until a
|
63
|
+
# table status changes to +ACTIVE+ and a table becomes available. That's
|
64
|
+
# why Dynamoid is polling a table status and returns results only when a
|
65
|
+
# table becomes available.
|
66
|
+
#
|
67
|
+
# Polling is configured with +Dynamoid::Config.sync_retry_max_times+ and
|
68
|
+
# +Dynamoid::Config.sync_retry_wait_seconds+ configuration options. If
|
69
|
+
# table creation takes more time than configured waiting time then
|
70
|
+
# Dynamoid stops polling and returns +true+.
|
71
|
+
#
|
72
|
+
# In order to return back asynchronous behaviour and not to wait until a
|
73
|
+
# table is created the +sync: false+ option should be specified.
|
74
|
+
#
|
75
|
+
# User.create_table(sync: false)
|
76
|
+
#
|
77
|
+
# Subsequent method calls for the same table will be ignored.
|
78
|
+
#
|
79
|
+
# @param options [Hash]
|
80
|
+
#
|
81
|
+
# @option options [Symbol] :table_name name of the table
|
82
|
+
# @option options [Symbol] :id hash key name of the table
|
83
|
+
# @option options [Symbol] :hash_key_type Dynamoid type of the hash key - +:string+, +:integer+ or any other scalar type
|
84
|
+
# @option options [Hash] :range_key a Hash with range key name and type in format +{ <name> => <type> }+ e.g. +{ last_name: :string }+
|
85
|
+
# @option options [String] :billing_mode billing mode of a table - either +PROVISIONED+ (default) or +PAY_PER_REQUEST+ (for On-Demand Mode)
|
86
|
+
# @option options [Integer] :read_capacity read capacity units for the table; does not work on existing tables and is ignored when billing mode is +PAY_PER_REQUEST+
|
87
|
+
# @option options [Integer] :write_capacity write capacity units for the table; does not work on existing tables and is ignored when billing mode is +PAY_PER_REQUEST+
|
88
|
+
# @option options [Hash] :local_secondary_indexes
|
89
|
+
# @option options [Hash] :global_secondary_indexes
|
90
|
+
# @option options [true|false] :sync specifies should the method call be synchronous and wait until a table is completely created
|
91
|
+
#
|
92
|
+
# @return [true|false] Whether a table created successfully
|
40
93
|
# @since 0.4.0
|
41
94
|
def create_table(options = {})
|
42
95
|
range_key_hash = if range_key
|
@@ -46,6 +99,7 @@ module Dynamoid
|
|
46
99
|
options = {
|
47
100
|
id: hash_key,
|
48
101
|
table_name: table_name,
|
102
|
+
billing_mode: capacity_mode,
|
49
103
|
write_capacity: write_capacity,
|
50
104
|
read_capacity: read_capacity,
|
51
105
|
range_key: range_key_hash,
|
@@ -54,14 +108,25 @@ module Dynamoid
|
|
54
108
|
global_secondary_indexes: global_secondary_indexes.values
|
55
109
|
}.merge(options)
|
56
110
|
|
57
|
-
Dynamoid.adapter.create_table(options[:table_name], options[:id], options)
|
111
|
+
created_successfuly = Dynamoid.adapter.create_table(options[:table_name], options[:id], options)
|
112
|
+
|
113
|
+
if created_successfuly && self.options[:expires]
|
114
|
+
attribute = self.options[:expires][:field]
|
115
|
+
Dynamoid.adapter.update_time_to_live(table_name, attribute)
|
116
|
+
end
|
58
117
|
end
|
59
118
|
|
60
|
-
# Deletes the table for the model
|
119
|
+
# Deletes the table for the model.
|
120
|
+
#
|
121
|
+
# Dynamoid deletes a table asynchronously and doesn't wait until a table
|
122
|
+
# is deleted completely.
|
123
|
+
#
|
124
|
+
# Subsequent method calls for the same table will be ignored.
|
61
125
|
def delete_table
|
62
126
|
Dynamoid.adapter.delete_table(table_name)
|
63
127
|
end
|
64
128
|
|
129
|
+
# @private
|
65
130
|
def from_database(attrs = {})
|
66
131
|
klass = choose_right_class(attrs)
|
67
132
|
attrs_undumped = Undumping.undump_attributes(attrs, klass.attributes)
|
@@ -70,95 +135,156 @@ module Dynamoid
|
|
70
135
|
|
71
136
|
# Create several models at once.
|
72
137
|
#
|
73
|
-
#
|
74
|
-
#
|
75
|
-
#
|
76
|
-
#
|
138
|
+
# users = User.import([{ name: 'a' }, { name: 'b' }])
|
139
|
+
#
|
140
|
+
# +import+ is a relatively low-level method and bypasses some
|
141
|
+
# mechanisms like callbacks and validation.
|
142
|
+
#
|
143
|
+
# It sets timestamp fields +created_at+ and +updated_at+ if they are
|
144
|
+
# blank. It sets a hash key field as well if it's blank. It expects that
|
145
|
+
# the hash key field is +string+ and sets a random UUID value if the field
|
146
|
+
# value is blank. All the field values are type casted to the declared
|
147
|
+
# types.
|
148
|
+
#
|
149
|
+
# It works efficiently and uses the `BatchWriteItem` operation. In order
|
150
|
+
# to cope with throttling it uses a backoff strategy if it's specified with
|
151
|
+
# `Dynamoid::Config.backoff` configuration option.
|
77
152
|
#
|
78
|
-
#
|
153
|
+
# Because of the nature of DynamoDB and its limits only 25 models can be
|
154
|
+
# saved at once. So multiple HTTP requests can be sent to DynamoDB.
|
79
155
|
#
|
80
|
-
# @
|
81
|
-
#
|
156
|
+
# @param array_of_attributes [Array<Hash>]
|
157
|
+
# @return [Array] Created models
|
82
158
|
def import(array_of_attributes)
|
83
159
|
Import.call(self, array_of_attributes)
|
84
160
|
end
|
85
161
|
|
86
162
|
# Create a model.
|
87
163
|
#
|
88
|
-
# Initializes a new
|
89
|
-
#
|
164
|
+
# Initializes a new model and immediately saves it to DynamoDB.
|
165
|
+
#
|
166
|
+
# User.create(first_name: 'Mark', last_name: 'Tyler')
|
167
|
+
#
|
90
168
|
# Accepts both Hash and Array of Hashes and can create several models.
|
91
169
|
#
|
92
|
-
#
|
170
|
+
# User.create([{ first_name: 'Alice' }, { first_name: 'Bob' }])
|
171
|
+
#
|
172
|
+
# Creates a model and pass it into a block to set other attributes.
|
173
|
+
#
|
174
|
+
# User.create(first_name: 'Mark') do |u|
|
175
|
+
# u.age = 21
|
176
|
+
# end
|
93
177
|
#
|
94
|
-
#
|
178
|
+
# Validates model and runs callbacks.
|
95
179
|
#
|
180
|
+
# @param attrs [Hash|Array[Hash]] Attributes of the models
|
181
|
+
# @param block [Proc] Block to process a document after initialization
|
182
|
+
# @return [Dynamoid::Document] The created document
|
96
183
|
# @since 0.2.0
|
97
|
-
def create(attrs = {})
|
184
|
+
def create(attrs = {}, &block)
|
98
185
|
if attrs.is_a?(Array)
|
99
|
-
attrs.map { |attr| create(attr) }
|
186
|
+
attrs.map { |attr| create(attr, &block) }
|
100
187
|
else
|
101
|
-
build(attrs).tap(&:save)
|
188
|
+
build(attrs, &block).tap(&:save)
|
102
189
|
end
|
103
190
|
end
|
104
191
|
|
105
|
-
# Create
|
106
|
-
#
|
107
|
-
# Initializes a new object and immediately saves it to the database.
|
108
|
-
# Raises an exception if validation failed.
|
109
|
-
# Accepts both Hash and Array of Hashes and can create several models.
|
110
|
-
#
|
111
|
-
# @param [Hash|Array[Hash]] attrs Attributes with which to create the object.
|
192
|
+
# Create a model.
|
112
193
|
#
|
113
|
-
#
|
194
|
+
# Initializes a new object and immediately saves it to the Dynamoid.
|
195
|
+
# Raises an exception +Dynamoid::Errors::DocumentNotValid+ if validation
|
196
|
+
# failed. Accepts both Hash and Array of Hashes and can create several
|
197
|
+
# models.
|
114
198
|
#
|
199
|
+
# @param attrs [Hash|Array[Hash]] Attributes with which to create the object.
|
200
|
+
# @param block [Proc] Block to process a document after initialization
|
201
|
+
# @return [Dynamoid::Document] The created document
|
115
202
|
# @since 0.2.0
|
116
|
-
def create!(attrs = {})
|
203
|
+
def create!(attrs = {}, &block)
|
117
204
|
if attrs.is_a?(Array)
|
118
|
-
attrs.map { |attr| create!(attr) }
|
205
|
+
attrs.map { |attr| create!(attr, &block) }
|
119
206
|
else
|
120
|
-
build(attrs).tap(&:save!)
|
207
|
+
build(attrs, &block).tap(&:save!)
|
121
208
|
end
|
122
209
|
end
|
123
210
|
|
124
211
|
# Update document with provided attributes.
|
125
212
|
#
|
126
|
-
# Instantiates document and saves changes.
|
127
|
-
#
|
213
|
+
# Instantiates document and saves changes. Runs validations and
|
214
|
+
# callbacks. Don't save changes if validation fails.
|
128
215
|
#
|
129
|
-
#
|
130
|
-
# @param [Scalar value] sort key, optional
|
131
|
-
# @param [Hash] attributes
|
216
|
+
# User.update('1', age: 26)
|
132
217
|
#
|
133
|
-
#
|
218
|
+
# If range key is declared for a model it should be passed as well:
|
134
219
|
#
|
135
|
-
#
|
136
|
-
#
|
220
|
+
# User.update('1', 'Tylor', age: 26)
|
221
|
+
#
|
222
|
+
# @param hash_key [Scalar value] hash key
|
223
|
+
# @param range_key_value [Scalar value] range key (optional)
|
224
|
+
# @param attrs [Hash]
|
225
|
+
# @return [Dynamoid::Document] Updated document
|
137
226
|
def update(hash_key, range_key_value = nil, attrs)
|
138
227
|
model = find(hash_key, range_key: range_key_value, consistent_read: true)
|
139
228
|
model.update_attributes(attrs)
|
140
229
|
model
|
141
230
|
end
|
142
231
|
|
232
|
+
# Update document with provided attributes.
|
233
|
+
#
|
234
|
+
# Instantiates document and saves changes. Runs validations and
|
235
|
+
# callbacks.
|
236
|
+
#
|
237
|
+
# User.update!('1', age: 26)
|
238
|
+
#
|
239
|
+
# If range key is declared for a model it should be passed as well:
|
240
|
+
#
|
241
|
+
# User.update('1', 'Tylor', age: 26)
|
242
|
+
#
|
243
|
+
# Raises +Dynamoid::Errors::DocumentNotValid+ exception if validation fails.
|
244
|
+
#
|
245
|
+
# @param hash_key [Scalar value] hash key
|
246
|
+
# @param range_key_value [Scalar value] range key (optional)
|
247
|
+
# @param attrs [Hash]
|
248
|
+
# @return [Dynamoid::Document] Updated document
|
249
|
+
def update!(hash_key, range_key_value = nil, attrs)
|
250
|
+
model = find(hash_key, range_key: range_key_value, consistent_read: true)
|
251
|
+
model.update_attributes!(attrs)
|
252
|
+
model
|
253
|
+
end
|
254
|
+
|
143
255
|
# Update document.
|
144
256
|
#
|
145
|
-
#
|
146
|
-
#
|
147
|
-
#
|
148
|
-
# If a document doesn't exist or specified conditions failed - returns `nil`
|
257
|
+
# Doesn't run validations and callbacks.
|
258
|
+
#
|
259
|
+
# User.update_fields('1', age: 26)
|
149
260
|
#
|
150
|
-
#
|
151
|
-
# @param [Scalar value] sort key (optional)
|
152
|
-
# @param [Hash] attributes
|
153
|
-
# @param [Hash] conditions
|
261
|
+
# If range key is declared for a model it should be passed as well:
|
154
262
|
#
|
155
|
-
#
|
263
|
+
# User.update_fields('1', 'Tylor', age: 26)
|
156
264
|
#
|
157
|
-
#
|
158
|
-
#
|
265
|
+
# Can make a conditional update so a document will be updated only if it
|
266
|
+
# meets the specified conditions. Conditions can be specified as a +Hash+
|
267
|
+
# with +:if+ key:
|
159
268
|
#
|
160
|
-
#
|
161
|
-
#
|
269
|
+
# User.update_fields('1', { age: 26 }, if: { version: 1 })
|
270
|
+
#
|
271
|
+
# Here +User+ model has an integer +version+ field and the document will
|
272
|
+
# be updated only if the +version+ attribute currently has value 1.
|
273
|
+
#
|
274
|
+
# If a document with specified hash and range keys doesn't exist or
|
275
|
+
# conditions were specified and failed the method call returns +nil+.
|
276
|
+
#
|
277
|
+
# +update_fields+ uses the +UpdateItem+ operation so it saves changes and
|
278
|
+
# loads an updated document back with one HTTP request.
|
279
|
+
#
|
280
|
+
# Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
|
281
|
+
# attributes is not on the model
|
282
|
+
#
|
283
|
+
# @param hash_key_value [Scalar value] hash key
|
284
|
+
# @param range_key_value [Scalar value] range key (optional)
|
285
|
+
# @param attrs [Hash]
|
286
|
+
# @param conditions [Hash] (optional)
|
287
|
+
# @return [Dynamoid::Document|nil] Updated document
|
162
288
|
def update_fields(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
|
163
289
|
optional_params = [range_key_value, attrs, conditions].compact
|
164
290
|
if optional_params.first.is_a?(Hash)
|
@@ -176,25 +302,40 @@ module Dynamoid
|
|
176
302
|
conditions: conditions)
|
177
303
|
end
|
178
304
|
|
179
|
-
# Update existing document or create new one.
|
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
|
+
# User.upsert('1', age: 26)
|
312
|
+
#
|
313
|
+
# If range key is declared for a model it should be passed as well:
|
314
|
+
#
|
315
|
+
# User.upsert('1', 'Tylor', age: 26)
|
180
316
|
#
|
181
|
-
#
|
182
|
-
#
|
317
|
+
# Can make a conditional update so a document will be updated only if it
|
318
|
+
# meets the specified conditions. Conditions can be specified as a +Hash+
|
319
|
+
# with +:if+ key:
|
183
320
|
#
|
184
|
-
#
|
185
|
-
# Changes attibutes and loads new document version with one API call.
|
186
|
-
# Doesn't run validations and callbacks. Can make conditional update.
|
187
|
-
# If specified conditions failed - returns `nil`.
|
321
|
+
# User.upsert('1', { age: 26 }, if: { version: 1 })
|
188
322
|
#
|
189
|
-
#
|
190
|
-
#
|
191
|
-
# @param [Hash] attributes
|
192
|
-
# @param [Hash] conditions
|
323
|
+
# Here +User+ model has an integer +version+ field and the document will
|
324
|
+
# be updated only if the +version+ attribute currently has value 1.
|
193
325
|
#
|
194
|
-
#
|
326
|
+
# If conditions were specified and failed the method call returns +nil+.
|
195
327
|
#
|
196
|
-
#
|
197
|
-
#
|
328
|
+
# +upsert+ uses the +UpdateItem+ operation so it saves changes and loads
|
329
|
+
# an updated document back with one HTTP request.
|
330
|
+
#
|
331
|
+
# Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
|
332
|
+
# attributes is not on the model
|
333
|
+
#
|
334
|
+
# @param hash_key_value [Scalar value] hash key
|
335
|
+
# @param range_key_value [Scalar value] range key (optional)
|
336
|
+
# @param attrs [Hash]
|
337
|
+
# @param conditions [Hash] (optional)
|
338
|
+
# @return [Dynamoid::Document|nil] Updated document
|
198
339
|
def upsert(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
|
199
340
|
optional_params = [range_key_value, attrs, conditions].compact
|
200
341
|
if optional_params.first.is_a?(Hash)
|
@@ -212,19 +353,27 @@ module Dynamoid
|
|
212
353
|
conditions: conditions)
|
213
354
|
end
|
214
355
|
|
215
|
-
# Increase numeric field by specified value.
|
356
|
+
# Increase a numeric field by specified value.
|
357
|
+
#
|
358
|
+
# User.inc('1', age: 2)
|
216
359
|
#
|
217
360
|
# Can update several fields at once.
|
218
|
-
# Uses efficient low-level `UpdateItem` API call.
|
219
361
|
#
|
220
|
-
#
|
221
|
-
#
|
222
|
-
#
|
362
|
+
# User.inc('1', age: 2, version: 1)
|
363
|
+
#
|
364
|
+
# If range key is declared for a model it should be passed as well:
|
365
|
+
#
|
366
|
+
# User.inc('1', 'Tylor', age: 2)
|
223
367
|
#
|
224
|
-
#
|
368
|
+
# Uses efficient low-level +UpdateItem+ operation and does only one HTTP
|
369
|
+
# request.
|
225
370
|
#
|
226
|
-
#
|
227
|
-
#
|
371
|
+
# Doesn't run validations and callbacks. Doesn't update +created_at+ and
|
372
|
+
# +updated_at+ as well.
|
373
|
+
#
|
374
|
+
# @param hash_key_value [Scalar value] hash key
|
375
|
+
# @param range_key_value [Scalar value] range key (optional)
|
376
|
+
# @param counters [Hash] value to increase by
|
228
377
|
def inc(hash_key_value, range_key_value = nil, counters)
|
229
378
|
options = if range_key
|
230
379
|
value_casted = TypeCasting.cast_field(range_key_value, attributes[range_key])
|
@@ -245,8 +394,17 @@ module Dynamoid
|
|
245
394
|
end
|
246
395
|
end
|
247
396
|
|
248
|
-
#
|
397
|
+
# Update document timestamps.
|
398
|
+
#
|
399
|
+
# Set +updated_at+ attribute to current DateTime.
|
400
|
+
#
|
401
|
+
# post.touch
|
402
|
+
#
|
403
|
+
# Can update another field in addition with the same timestamp if it's name passed as argument.
|
249
404
|
#
|
405
|
+
# user.touch(:last_login_at)
|
406
|
+
#
|
407
|
+
# @param name [Symbol] attribute name to update (optional)
|
250
408
|
def touch(name = nil)
|
251
409
|
now = DateTime.now
|
252
410
|
self.updated_at = now
|
@@ -254,18 +412,72 @@ module Dynamoid
|
|
254
412
|
save
|
255
413
|
end
|
256
414
|
|
257
|
-
# Is this object persisted in
|
415
|
+
# Is this object persisted in DynamoDB?
|
416
|
+
#
|
417
|
+
# user = User.new
|
418
|
+
# user.persisted? # => false
|
258
419
|
#
|
420
|
+
# user.save
|
421
|
+
# user.persisted? # => true
|
422
|
+
#
|
423
|
+
# @return [true|false]
|
259
424
|
# @since 0.2.0
|
260
425
|
def persisted?
|
261
|
-
!new_record?
|
426
|
+
!(new_record? || @destroyed)
|
262
427
|
end
|
263
428
|
|
264
|
-
#
|
429
|
+
# Create new model or persist changes.
|
430
|
+
#
|
431
|
+
# Run the validation and callbacks. Returns +true+ if saving is successful
|
432
|
+
# and +false+ otherwise.
|
433
|
+
#
|
434
|
+
# user = User.new
|
435
|
+
# user.save # => true
|
436
|
+
#
|
437
|
+
# user.age = 26
|
438
|
+
# user.save # => true
|
439
|
+
#
|
440
|
+
# Validation can be skipped with +validate: false+ option:
|
441
|
+
#
|
442
|
+
# user = User.new(age: -1)
|
443
|
+
# user.save(validate: false) # => true
|
265
444
|
#
|
445
|
+
# +save+ by default sets timestamps attributes - +created_at+ and
|
446
|
+
# +updated_at+ when creates new model and updates +updated_at+ attribute
|
447
|
+
# when update already existing one.
|
448
|
+
#
|
449
|
+
# Changing +updated_at+ attribute at updating a model can be skipped with
|
450
|
+
# +touch: false+ option:
|
451
|
+
#
|
452
|
+
# user.save(touch: false)
|
453
|
+
#
|
454
|
+
# If a model is new and hash key (+id+ by default) is not assigned yet
|
455
|
+
# it was assigned implicitly with random UUID value.
|
456
|
+
#
|
457
|
+
# If +lock_version+ attribute is declared it will be incremented. If it's blank then it will be initialized with 1.
|
458
|
+
#
|
459
|
+
# +save+ method call raises +Dynamoid::Errors::RecordNotUnique+ exception
|
460
|
+
# if primary key (hash key + optional range key) already exists in a
|
461
|
+
# table.
|
462
|
+
#
|
463
|
+
# +save+ method call raises +Dynamoid::Errors::StaleObjectError+ exception
|
464
|
+
# if there is +lock_version+ attribute and the document in a table was
|
465
|
+
# already changed concurrently and +lock_version+ was consequently
|
466
|
+
# increased.
|
467
|
+
#
|
468
|
+
# When a table is not created yet the first +save+ method call will create
|
469
|
+
# a table. It's useful in test environment to avoid explicit table
|
470
|
+
# creation.
|
471
|
+
#
|
472
|
+
# @param options [Hash] (optional)
|
473
|
+
# @option options [true|false] :validate validate a model or not - +true+ by default (optional)
|
474
|
+
# @option options [true|false] :touch update tiemstamps fields or not - +true+ by default (optional)
|
475
|
+
# @return [true|false] Whether saving successful or not
|
266
476
|
# @since 0.2.0
|
267
|
-
def save(
|
268
|
-
self.class.create_table
|
477
|
+
def save(options = {})
|
478
|
+
self.class.create_table(sync: true)
|
479
|
+
|
480
|
+
@_touch_record = options[:touch]
|
269
481
|
|
270
482
|
if new_record?
|
271
483
|
run_callbacks(:create) do
|
@@ -280,20 +492,35 @@ module Dynamoid
|
|
280
492
|
end
|
281
493
|
end
|
282
494
|
|
283
|
-
#
|
495
|
+
# Update multiple attributes at once, saving the object once the updates
|
496
|
+
# are complete. Returns +true+ if saving is successful and +false+
|
497
|
+
# otherwise.
|
284
498
|
#
|
285
|
-
#
|
499
|
+
# user.update_attributes(age: 27, last_name: 'Tylor')
|
286
500
|
#
|
501
|
+
# Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
|
502
|
+
# attributes is not on the model
|
503
|
+
#
|
504
|
+
# @param attributes [Hash] a hash of attributes to update
|
505
|
+
# @return [true|false] Whether updating successful or not
|
287
506
|
# @since 0.2.0
|
288
507
|
def update_attributes(attributes)
|
289
508
|
attributes.each { |attribute, value| write_attribute(attribute, value) }
|
290
509
|
save
|
291
510
|
end
|
292
511
|
|
293
|
-
#
|
294
|
-
#
|
512
|
+
# Update multiple attributes at once, saving the object once the updates
|
513
|
+
# are complete.
|
514
|
+
#
|
515
|
+
# user.update_attributes!(age: 27, last_name: 'Tylor')
|
516
|
+
#
|
517
|
+
# Raises a +Dynamoid::Errors::DocumentNotValid+ exception if some vaidation
|
518
|
+
# fails.
|
295
519
|
#
|
296
|
-
#
|
520
|
+
# Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
|
521
|
+
# attributes is not on the model
|
522
|
+
#
|
523
|
+
# @param attributes [Hash] a hash of attributes to update
|
297
524
|
def update_attributes!(attributes)
|
298
525
|
attributes.each { |attribute, value| write_attribute(attribute, value) }
|
299
526
|
save!
|
@@ -301,20 +528,71 @@ module Dynamoid
|
|
301
528
|
|
302
529
|
# Update a single attribute, saving the object afterwards.
|
303
530
|
#
|
304
|
-
#
|
305
|
-
#
|
531
|
+
# Returns +true+ if saving is successful and +false+ otherwise.
|
532
|
+
#
|
533
|
+
# user.update_attribute(:last_name, 'Tylor')
|
306
534
|
#
|
535
|
+
# Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
|
536
|
+
# attributes is not on the model
|
537
|
+
#
|
538
|
+
# @param attribute [Symbol] attribute name to update
|
539
|
+
# @param value [Object] the value to assign it
|
540
|
+
# @return [Dynamoid::Document] self
|
307
541
|
# @since 0.2.0
|
308
542
|
def update_attribute(attribute, value)
|
309
543
|
write_attribute(attribute, value)
|
310
544
|
save
|
311
545
|
end
|
312
546
|
|
547
|
+
# Update a model.
|
548
|
+
#
|
549
|
+
# Runs validation and callbacks. Reloads all attribute values.
|
550
|
+
#
|
551
|
+
# Accepts mandatory block in order to specify operations which will modify
|
552
|
+
# attributes. Supports following operations: +add+, +delete+ and +set+.
|
553
|
+
#
|
554
|
+
# Operation +add+ just adds a value for numeric attributes and join
|
555
|
+
# collections if attribute is a collection (one of +array+, +set+ or
|
556
|
+
# +map+).
|
313
557
|
#
|
314
|
-
#
|
315
|
-
#
|
558
|
+
# user.update do |t|
|
559
|
+
# t.add(age: 1, followers_count: 5)
|
560
|
+
# t.add(hobbies: ['skying', 'climbing'])
|
561
|
+
# end
|
316
562
|
#
|
563
|
+
# Operation +delete+ is applied to collection attribute types and
|
564
|
+
# substructs one collection from another.
|
317
565
|
#
|
566
|
+
# user.update do |t|
|
567
|
+
# t.delete(hobbies: ['skying'])
|
568
|
+
# end
|
569
|
+
#
|
570
|
+
# Operation +set+ just changes an attribute value:
|
571
|
+
#
|
572
|
+
# user.update do |t|
|
573
|
+
# t.set(age: 21)
|
574
|
+
# end
|
575
|
+
#
|
576
|
+
# All the operations works like +ADD+, +DELETE+ and +PUT+ actions supported
|
577
|
+
# by +AttributeUpdates+
|
578
|
+
# {parameter}[https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LegacyConditionalParameters.AttributeUpdates.html]
|
579
|
+
# of +UpdateItem+ operation.
|
580
|
+
#
|
581
|
+
# Can update a model conditionaly:
|
582
|
+
#
|
583
|
+
# user.update(if: { age: 20 }) do |t|
|
584
|
+
# t.add(age: 1)
|
585
|
+
# end
|
586
|
+
#
|
587
|
+
# If a document doesn't meet conditions it raises
|
588
|
+
# +Dynamoid::Errors::StaleObjectError+ exception.
|
589
|
+
#
|
590
|
+
# It will increment the +lock_version+ attribute if a table has the column,
|
591
|
+
# but will not check it. Thus, a concurrent +save+ call will never cause an
|
592
|
+
# +update!+ to fail, but an +update!+ may cause a concurrent +save+ to
|
593
|
+
# fail.
|
594
|
+
#
|
595
|
+
# @param conditions [Hash] Conditions on model attributes to make a conditional update (optional)
|
318
596
|
def update!(conditions = {})
|
319
597
|
run_callbacks(:update) do
|
320
598
|
options = range_key ? { range_key: Dumping.dump_field(read_attribute(range_key), self.class.attributes[range_key]) } : {}
|
@@ -336,9 +614,56 @@ module Dynamoid
|
|
336
614
|
raise Dynamoid::Errors::StaleObjectError.new(self, 'update')
|
337
615
|
end
|
338
616
|
end
|
339
|
-
|
340
617
|
end
|
341
618
|
|
619
|
+
# Update a model.
|
620
|
+
#
|
621
|
+
# Runs validation and callbacks. Reloads all attribute values.
|
622
|
+
#
|
623
|
+
# Accepts mandatory block in order to specify operations which will modify
|
624
|
+
# attributes. Supports following operations: +add+, +delete+ and +set+.
|
625
|
+
#
|
626
|
+
# Operation +add+ just adds a value for numeric attributes and join
|
627
|
+
# collections if attribute is a collection (one of +array+, +set+ or
|
628
|
+
# +map+).
|
629
|
+
#
|
630
|
+
# user.update do |t|
|
631
|
+
# t.add(age: 1, followers_count: 5)
|
632
|
+
# t.add(hobbies: ['skying', 'climbing'])
|
633
|
+
# end
|
634
|
+
#
|
635
|
+
# Operation +delete+ is applied to collection attribute types and
|
636
|
+
# substructs one collection from another.
|
637
|
+
#
|
638
|
+
# user.update do |t|
|
639
|
+
# t.delete(hobbies: ['skying'])
|
640
|
+
# end
|
641
|
+
#
|
642
|
+
# Operation +set+ just changes an attribute value:
|
643
|
+
#
|
644
|
+
# user.update do |t|
|
645
|
+
# t.set(age: 21)
|
646
|
+
# end
|
647
|
+
#
|
648
|
+
# All the operations works like +ADD+, +DELETE+ and +PUT+ actions supported
|
649
|
+
# by +AttributeUpdates+
|
650
|
+
# {parameter}[https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LegacyConditionalParameters.AttributeUpdates.html]
|
651
|
+
# of +UpdateItem+ operation.
|
652
|
+
#
|
653
|
+
# Can update a model conditionaly:
|
654
|
+
#
|
655
|
+
# user.update(if: { age: 20 }) do |t|
|
656
|
+
# t.add(age: 1)
|
657
|
+
# end
|
658
|
+
#
|
659
|
+
# If a document doesn't meet conditions it just returns +false+. Otherwise it returns +true+.
|
660
|
+
#
|
661
|
+
# It will increment the +lock_version+ attribute if a table has the column,
|
662
|
+
# but will not check it. Thus, a concurrent +save+ call will never cause an
|
663
|
+
# +update!+ to fail, but an +update!+ may cause a concurrent +save+ to
|
664
|
+
# fail.
|
665
|
+
#
|
666
|
+
# @param conditions [Hash] Conditions on model attributes to make a conditional update (optional)
|
342
667
|
def update(conditions = {}, &block)
|
343
668
|
update!(conditions, &block)
|
344
669
|
true
|
@@ -346,51 +671,117 @@ module Dynamoid
|
|
346
671
|
false
|
347
672
|
end
|
348
673
|
|
349
|
-
#
|
350
|
-
#
|
674
|
+
# Change numeric attribute value.
|
675
|
+
#
|
676
|
+
# Initializes attribute to zero if +nil+ and adds the specified value (by
|
677
|
+
# default is 1). Only makes sense for number-based attributes.
|
678
|
+
#
|
679
|
+
# user.increment(:followers_count)
|
680
|
+
# user.increment(:followers_count, 2)
|
681
|
+
#
|
682
|
+
# @param attribute [Symbol] attribute name
|
683
|
+
# @param by [Numeric] value to add (optional)
|
684
|
+
# @return [Dynamoid::Document] self
|
351
685
|
def increment(attribute, by = 1)
|
352
686
|
self[attribute] ||= 0
|
353
687
|
self[attribute] += by
|
354
688
|
self
|
355
689
|
end
|
356
690
|
|
357
|
-
#
|
358
|
-
#
|
691
|
+
# Change numeric attribute value and save a model.
|
692
|
+
#
|
693
|
+
# Initializes attribute to zero if +nil+ and adds the specified value (by
|
694
|
+
# default is 1). Only makes sense for number-based attributes.
|
695
|
+
#
|
696
|
+
# user.increment!(:followers_count)
|
697
|
+
# user.increment!(:followers_count, 2)
|
698
|
+
#
|
699
|
+
# Returns +true+ if a model was saved and +false+ otherwise.
|
700
|
+
#
|
701
|
+
# @param attribute [Symbol] attribute name
|
702
|
+
# @param by [Numeric] value to add (optional)
|
703
|
+
# @return [true|false] whether saved model successfully
|
359
704
|
def increment!(attribute, by = 1)
|
360
705
|
increment(attribute, by)
|
361
706
|
save
|
362
707
|
end
|
363
708
|
|
364
|
-
#
|
365
|
-
#
|
709
|
+
# Change numeric attribute value.
|
710
|
+
#
|
711
|
+
# Initializes attribute to zero if +nil+ and subtracts the specified value
|
712
|
+
# (by default is 1). Only makes sense for number-based attributes.
|
713
|
+
#
|
714
|
+
# user.decrement(:followers_count)
|
715
|
+
# user.decrement(:followers_count, 2)
|
716
|
+
#
|
717
|
+
# @param attribute [Symbol] attribute name
|
718
|
+
# @param by [Numeric] value to subtract (optional)
|
719
|
+
# @return [Dynamoid::Document] self
|
366
720
|
def decrement(attribute, by = 1)
|
367
721
|
self[attribute] ||= 0
|
368
722
|
self[attribute] -= by
|
369
723
|
self
|
370
724
|
end
|
371
725
|
|
372
|
-
#
|
373
|
-
#
|
726
|
+
# Change numeric attribute value and save a model.
|
727
|
+
#
|
728
|
+
# Initializes attribute to zero if +nil+ and subtracts the specified value
|
729
|
+
# (by default is 1). Only makes sense for number-based attributes.
|
730
|
+
#
|
731
|
+
# user.decrement!(:followers_count)
|
732
|
+
# user.decrement!(:followers_count, 2)
|
733
|
+
#
|
734
|
+
# Returns +true+ if a model was saved and +false+ otherwise.
|
735
|
+
#
|
736
|
+
# @param attribute [Symbol] attribute name
|
737
|
+
# @param by [Numeric] value to subtract (optional)
|
738
|
+
# @return [true|false] whether saved model successfully
|
374
739
|
def decrement!(attribute, by = 1)
|
375
740
|
decrement(attribute, by)
|
376
741
|
save
|
377
742
|
end
|
378
743
|
|
379
|
-
# Delete
|
744
|
+
# Delete a model.
|
745
|
+
#
|
746
|
+
# Runs callbacks.
|
747
|
+
#
|
748
|
+
# Supports optimistic locking with the +lock_version+ attribute and doesn't
|
749
|
+
# delete a model if it's already changed.
|
380
750
|
#
|
751
|
+
# Returns +true+ if deleted successfully and +false+ otherwise.
|
752
|
+
#
|
753
|
+
# @return [true|false] whether deleted successfully
|
381
754
|
# @since 0.2.0
|
382
755
|
def destroy
|
383
756
|
ret = run_callbacks(:destroy) do
|
384
757
|
delete
|
385
758
|
end
|
759
|
+
|
760
|
+
@destroyed = true
|
761
|
+
|
386
762
|
ret == false ? false : self
|
387
763
|
end
|
388
764
|
|
765
|
+
# Delete a model.
|
766
|
+
#
|
767
|
+
# Runs callbacks.
|
768
|
+
#
|
769
|
+
# Supports optimistic locking with the +lock_version+ attribute and doesn't
|
770
|
+
# delete a model if it's already changed.
|
771
|
+
#
|
772
|
+
# Raises +Dynamoid::Errors::RecordNotDestroyed+ exception if model deleting
|
773
|
+
# failed.
|
389
774
|
def destroy!
|
390
775
|
destroy || (raise Dynamoid::Errors::RecordNotDestroyed, self)
|
391
776
|
end
|
392
777
|
|
393
|
-
# Delete
|
778
|
+
# Delete a model.
|
779
|
+
#
|
780
|
+
# Supports optimistic locking with the +lock_version+ attribute and doesn't
|
781
|
+
# delete a model if it's already changed.
|
782
|
+
#
|
783
|
+
# Raises +Dynamoid::Errors::StaleObjectError+ exception if cannot delete a
|
784
|
+
# model.
|
394
785
|
#
|
395
786
|
# @since 0.2.0
|
396
787
|
def delete
|
@@ -407,7 +798,14 @@ module Dynamoid
|
|
407
798
|
end
|
408
799
|
options[:conditions] = conditions
|
409
800
|
end
|
801
|
+
|
802
|
+
@destroyed = true
|
803
|
+
|
410
804
|
Dynamoid.adapter.delete(self.class.table_name, hash_key, options)
|
805
|
+
|
806
|
+
self.class.associations.each do |name, options|
|
807
|
+
send(name).disassociate_source
|
808
|
+
end
|
411
809
|
rescue Dynamoid::Errors::ConditionalCheckFailedException
|
412
810
|
raise Dynamoid::Errors::StaleObjectError.new(self, 'delete')
|
413
811
|
end
|