dynamoid 3.3.0 → 3.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|