dynamoid 3.2.0 → 3.3.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.
@@ -70,36 +70,6 @@ module Dynamoid #:nodoc:
70
70
  Dynamoid.adapter.count(table_name)
71
71
  end
72
72
 
73
- # Initialize a new object and immediately save it to the database.
74
- #
75
- # @param [Hash] attrs Attributes with which to create the object.
76
- #
77
- # @return [Dynamoid::Document] the saved document
78
- #
79
- # @since 0.2.0
80
- def create(attrs = {})
81
- if attrs.is_a?(Array)
82
- attrs.map { |attr| create(attr) }
83
- else
84
- build(attrs).tap(&:save)
85
- end
86
- end
87
-
88
- # Initialize a new object and immediately save it to the database. Raise an exception if persistence failed.
89
- #
90
- # @param [Hash] attrs Attributes with which to create the object.
91
- #
92
- # @return [Dynamoid::Document] the saved document
93
- #
94
- # @since 0.2.0
95
- def create!(attrs = {})
96
- if attrs.is_a?(Array)
97
- attrs.map { |attr| create!(attr) }
98
- else
99
- build(attrs).tap(&:save!)
100
- end
101
- end
102
-
103
73
  # Initialize a new object.
104
74
  #
105
75
  # @param [Hash] attrs Attributes with which to create the object.
@@ -145,161 +115,6 @@ module Dynamoid #:nodoc:
145
115
  end
146
116
  end
147
117
 
148
- # Update document with provided values.
149
- # Instantiates document and saves changes. Runs validations and callbacks.
150
- #
151
- # @param [Scalar value] partition key
152
- # @param [Scalar value] sort key, optional
153
- # @param [Hash] attributes
154
- #
155
- # @return [Dynamoid::Doument] updated document
156
- #
157
- # @example Update document
158
- # Post.update(101, read: true)
159
- def update(hash_key, range_key_value = nil, attrs)
160
- model = find(hash_key, range_key: range_key_value, consistent_read: true)
161
- model.update_attributes(attrs)
162
- model
163
- end
164
-
165
- # Update document.
166
- # Uses efficient low-level `UpdateItem` API call.
167
- # Changes attibutes and loads new document version with one API call.
168
- # Doesn't run validations and callbacks. Can make conditional update.
169
- # If a document doesn't exist or specified conditions failed - returns `nil`
170
- #
171
- # @param [Scalar value] partition key
172
- # @param [Scalar value] sort key (optional)
173
- # @param [Hash] attributes
174
- # @param [Hash] conditions
175
- #
176
- # @return [Dynamoid::Document/nil] updated document
177
- #
178
- # @example Update document
179
- # Post.update_fields(101, read: true)
180
- #
181
- # @example Update document with condition
182
- # Post.update_fields(101, { read: true }, if: { version: 1 })
183
- def update_fields(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
184
- optional_params = [range_key_value, attrs, conditions].compact
185
- if optional_params.first.is_a?(Hash)
186
- range_key_value = nil
187
- attrs, conditions = optional_params[0..1]
188
- else
189
- range_key_value = optional_params.first
190
- attrs, conditions = optional_params[1..2]
191
- end
192
-
193
- options = if range_key
194
- value_casted = TypeCasting.cast_field(range_key_value, attributes[range_key])
195
- value_dumped = Dumping.dump_field(value_casted, attributes[range_key])
196
- { range_key: value_dumped }
197
- else
198
- {}
199
- end
200
-
201
- (conditions[:if_exists] ||= {})[hash_key] = hash_key_value
202
- options[:conditions] = conditions
203
-
204
- attrs = attrs.symbolize_keys
205
- if Dynamoid::Config.timestamps
206
- attrs[:updated_at] ||= DateTime.now.in_time_zone(Time.zone)
207
- end
208
-
209
- begin
210
- new_attrs = Dynamoid.adapter.update_item(table_name, hash_key_value, options) do |t|
211
- attrs.each do |k, v|
212
- value_casted = TypeCasting.cast_field(v, attributes[k])
213
- value_dumped = Dumping.dump_field(value_casted, attributes[k])
214
- t.set(k => value_dumped)
215
- end
216
- end
217
- attrs_undumped = Undumping.undump_attributes(new_attrs, attributes)
218
- new(attrs_undumped)
219
- rescue Dynamoid::Errors::ConditionalCheckFailedException
220
- end
221
- end
222
-
223
- # Update existing document or create new one.
224
- # Similar to `.update_fields`. The only diffirence is creating new document.
225
- #
226
- # Uses efficient low-level `UpdateItem` API call.
227
- # Changes attibutes and loads new document version with one API call.
228
- # Doesn't run validations and callbacks. Can make conditional update.
229
- # If specified conditions failed - returns `nil`
230
- #
231
- # @param [Scalar value] partition key
232
- # @param [Scalar value] sort key (optional)
233
- # @param [Hash] attributes
234
- # @param [Hash] conditions
235
- #
236
- # @return [Dynamoid::Document/nil] updated document
237
- #
238
- # @example Update document
239
- # Post.update(101, read: true)
240
- #
241
- # @example Update document
242
- # Post.upsert(101, read: true)
243
- def upsert(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
244
- optional_params = [range_key_value, attrs, conditions].compact
245
- if optional_params.first.is_a?(Hash)
246
- range_key_value = nil
247
- attrs, conditions = optional_params[0..1]
248
- else
249
- range_key_value = optional_params.first
250
- attrs, conditions = optional_params[1..2]
251
- end
252
-
253
- options = if range_key
254
- value_casted = TypeCasting.cast_field(range_key_value, attributes[range_key])
255
- value_dumped = Dumping.dump_field(value_casted, attributes[range_key])
256
- { range_key: value_dumped }
257
- else
258
- {}
259
- end
260
-
261
- options[:conditions] = conditions
262
-
263
- attrs = attrs.symbolize_keys
264
- if Dynamoid::Config.timestamps
265
- attrs[:updated_at] ||= DateTime.now.in_time_zone(Time.zone)
266
- end
267
-
268
- begin
269
- new_attrs = Dynamoid.adapter.update_item(table_name, hash_key_value, options) do |t|
270
- attrs.each do |k, v|
271
- value_casted = TypeCasting.cast_field(v, attributes[k])
272
- value_dumped = Dumping.dump_field(value_casted, attributes[k])
273
-
274
- t.set(k => value_dumped)
275
- end
276
- end
277
-
278
- attrs_undumped = Undumping.undump_attributes(new_attrs, attributes)
279
- new(attrs_undumped)
280
- rescue Dynamoid::Errors::ConditionalCheckFailedException
281
- end
282
- end
283
-
284
- def inc(hash_key_value, range_key_value = nil, counters)
285
- options = if range_key
286
- value_casted = TypeCasting.cast_field(range_key_value, attributes[range_key])
287
- value_dumped = Dumping.dump_field(value_casted, attributes[range_key])
288
- { range_key: value_dumped }
289
- else
290
- {}
291
- end
292
-
293
- Dynamoid.adapter.update_item(table_name, hash_key_value, options) do |t|
294
- counters.each do |k, v|
295
- value_casted = TypeCasting.cast_field(v, attributes[k])
296
- value_dumped = Dumping.dump_field(value_casted, attributes[k])
297
-
298
- t.add(k => value_dumped)
299
- end
300
- end
301
- end
302
-
303
118
  def deep_subclasses
304
119
  subclasses + subclasses.map(&:deep_subclasses).flatten
305
120
  end
@@ -323,13 +138,14 @@ module Dynamoid #:nodoc:
323
138
  @associations ||= {}
324
139
  @attributes_before_type_cast ||= {}
325
140
 
326
- attrs_with_defaults = {}
327
- self.class.attributes.each do |attribute, options|
328
- attrs_with_defaults[attribute] = if attrs.key?(attribute)
329
- attrs[attribute]
330
- elsif options.key?(:default)
331
- evaluate_default_value(options[:default])
332
- end
141
+ attrs_with_defaults = self.class.attributes.reduce({}) do |res, (attribute, options)|
142
+ if attrs.key?(attribute)
143
+ res.merge(attribute => attrs[attribute])
144
+ elsif options.key?(:default)
145
+ res.merge(attribute => evaluate_default_value(options[:default]))
146
+ else
147
+ res
148
+ end
333
149
  end
334
150
 
335
151
  attrs_virtual = attrs.slice(*(attrs.keys - self.class.attributes.keys))
@@ -338,12 +154,6 @@ module Dynamoid #:nodoc:
338
154
  end
339
155
  end
340
156
 
341
- def load(attrs)
342
- attrs.each do |key, value|
343
- send("#{key}=", value) if respond_to?("#{key}=")
344
- end
345
- end
346
-
347
157
  # An object is equal to another object if their ids are equal.
348
158
  #
349
159
  # @since 0.2.0
@@ -365,24 +175,6 @@ module Dynamoid #:nodoc:
365
175
  hash_key.hash ^ range_value.hash
366
176
  end
367
177
 
368
- # Reload an object from the database -- if you suspect the object has changed in the datastore and you need those
369
- # changes to be reflected immediately, you would call this method. This is a consistent read.
370
- #
371
- # @return [Dynamoid::Document] the document this method was called on
372
- #
373
- # @since 0.2.0
374
- def reload
375
- options = { consistent_read: true }
376
-
377
- if self.class.range_key
378
- options[:range_key] = range_value
379
- end
380
-
381
- self.attributes = self.class.find(hash_key, options).attributes
382
- @associations.values.each(&:reset)
383
- self
384
- end
385
-
386
178
  # Return an object's hash key, regardless of what it might be called to the object.
387
179
  #
388
180
  # @since 0.4.0
@@ -52,6 +52,8 @@ module Dynamoid #:nodoc:
52
52
  end
53
53
  self.attributes = attributes.merge(name => { type: type }.merge(options))
54
54
 
55
+ define_attribute_method(name) # Dirty API
56
+
55
57
  generated_methods.module_eval do
56
58
  define_method(named) { read_attribute(named) }
57
59
  define_method("#{named}?") do
@@ -85,6 +87,10 @@ module Dynamoid #:nodoc:
85
87
  field = field.to_sym
86
88
  attributes.delete(field) || raise('No such field')
87
89
 
90
+ # Dirty API
91
+ undefine_attribute_methods
92
+ define_attribute_methods attributes.keys
93
+
88
94
  generated_methods.module_eval do
89
95
  remove_method field
90
96
  remove_method :"#{field}="
@@ -121,6 +127,8 @@ module Dynamoid #:nodoc:
121
127
  association.reset
122
128
  end
123
129
 
130
+ attribute_will_change!(name) # Dirty API
131
+
124
132
  @attributes_before_type_cast[name] = value
125
133
 
126
134
  value_casted = TypeCasting.cast_field(value, self.class.attributes[name])
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module Loadable
5
+ extend ActiveSupport::Concern
6
+
7
+ def load(attrs)
8
+ attrs.each do |key, value|
9
+ send("#{key}=", value) if respond_to?("#{key}=")
10
+ end
11
+ end
12
+
13
+ # Reload an object from the database -- if you suspect the object has changed in the datastore and you need those
14
+ # changes to be reflected immediately, you would call this method. This is a consistent read.
15
+ #
16
+ # @return [Dynamoid::Document] the document this method was called on
17
+ #
18
+ # @since 0.2.0
19
+ def reload
20
+ options = { consistent_read: true }
21
+
22
+ if self.class.range_key
23
+ options[:range_key] = range_value
24
+ end
25
+
26
+ self.attributes = self.class.find(hash_key, options).attributes
27
+ @associations.values.each(&:reset)
28
+ self
29
+ end
30
+ end
31
+ end
@@ -4,6 +4,11 @@ require 'bigdecimal'
4
4
  require 'securerandom'
5
5
  require 'yaml'
6
6
 
7
+ require 'dynamoid/persistence/import'
8
+ require 'dynamoid/persistence/update_fields'
9
+ require 'dynamoid/persistence/upsert'
10
+ require 'dynamoid/persistence/save'
11
+
7
12
  # encoding: utf-8
8
13
  module Dynamoid
9
14
  # Persistence is responsible for dumping objects to and marshalling objects from the datastore. It tries to reserialize
@@ -23,7 +28,7 @@ module Dynamoid
23
28
  @table_name ||= [Dynamoid::Config.namespace.to_s, table_base_name].reject(&:empty?).join('_')
24
29
  end
25
30
 
26
- # Creates a table.
31
+ # Create a table.
27
32
  #
28
33
  # @param [Hash] options options to pass for table creation
29
34
  # @option options [Symbol] :id the id field for the table
@@ -58,63 +63,185 @@ module Dynamoid
58
63
  end
59
64
 
60
65
  def from_database(attrs = {})
61
- clazz = choose_right_class(attrs)
62
- attrs_undumped = Undumping.undump_attributes(attrs, clazz.attributes)
63
- clazz.new(attrs_undumped).tap { |r| r.new_record = false }
66
+ klass = choose_right_class(attrs)
67
+ attrs_undumped = Undumping.undump_attributes(attrs, klass.attributes)
68
+ klass.new(attrs_undumped).tap { |r| r.new_record = false }
64
69
  end
65
70
 
66
- # Creates several models at once.
67
- # Neither callbacks nor validations run.
68
- # It works efficiently because of using BatchWriteItem.
69
- #
70
- # Returns array of models
71
+ # Create several models at once.
71
72
  #
73
+ # Neither callbacks nor validations run.
74
+ # It works efficiently because of using `BatchWriteItem` API call.
75
+ # Return array of models.
72
76
  # Uses backoff specified by `Dynamoid::Config.backoff` config option
73
77
  #
74
- # @param [Array<Hash>] items
78
+ # @param [Array<Hash>] array_of_attributes
75
79
  #
76
80
  # @example
77
81
  # User.import([{ name: 'a' }, { name: 'b' }])
78
- def import(objects)
79
- documents = objects.map do |attrs|
80
- attrs = attrs.symbolize_keys
81
-
82
- if Dynamoid::Config.timestamps
83
- time_now = DateTime.now.in_time_zone(Time.zone)
84
- attrs[:created_at] ||= time_now
85
- attrs[:updated_at] ||= time_now
86
- end
82
+ def import(array_of_attributes)
83
+ Import.call(self, array_of_attributes)
84
+ end
87
85
 
88
- build(attrs).tap do |item|
89
- item.hash_key = SecureRandom.uuid if item.hash_key.blank?
90
- end
86
+ # Create a model.
87
+ #
88
+ # Initializes a new object and immediately saves it to the database.
89
+ # Validates model and runs callbacks: before_create, before_save, after_save and after_create.
90
+ # Accepts both Hash and Array of Hashes and can create several models.
91
+ #
92
+ # @param [Hash|Array[Hash]] attrs Attributes with which to create the object.
93
+ #
94
+ # @return [Dynamoid::Document] the saved document
95
+ #
96
+ # @since 0.2.0
97
+ def create(attrs = {})
98
+ if attrs.is_a?(Array)
99
+ attrs.map { |attr| create(attr) }
100
+ else
101
+ build(attrs).tap(&:save)
91
102
  end
103
+ end
92
104
 
93
- if Dynamoid.config.backoff
94
- backoff = nil
105
+ # Create new model.
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.
112
+ #
113
+ # @return [Dynamoid::Document] the saved document
114
+ #
115
+ # @since 0.2.0
116
+ def create!(attrs = {})
117
+ if attrs.is_a?(Array)
118
+ attrs.map { |attr| create!(attr) }
119
+ else
120
+ build(attrs).tap(&:save!)
121
+ end
122
+ end
95
123
 
96
- array = documents.map do |d|
97
- Dumping.dump_attributes(d.attributes, attributes)
98
- end
124
+ # Update document with provided attributes.
125
+ #
126
+ # Instantiates document and saves changes.
127
+ # Runs validations and callbacks.
128
+ #
129
+ # @param [Scalar value] partition key
130
+ # @param [Scalar value] sort key, optional
131
+ # @param [Hash] attributes
132
+ #
133
+ # @return [Dynamoid::Doument] updated document
134
+ #
135
+ # @example Update document
136
+ # Post.update(101, title: 'New title')
137
+ def update(hash_key, range_key_value = nil, attrs)
138
+ model = find(hash_key, range_key: range_key_value, consistent_read: true)
139
+ model.update_attributes(attrs)
140
+ model
141
+ end
99
142
 
100
- Dynamoid.adapter.batch_write_item(table_name, array) do |has_unprocessed_items|
101
- if has_unprocessed_items
102
- backoff ||= Dynamoid.config.build_backoff
103
- backoff.call
104
- else
105
- backoff = nil
106
- end
107
- end
143
+ # Update document.
144
+ #
145
+ # Uses efficient low-level `UpdateItem` API call.
146
+ # Changes attibutes and loads new document version with one API call.
147
+ # Doesn't run validations and callbacks. Can make conditional update.
148
+ # If a document doesn't exist or specified conditions failed - returns `nil`
149
+ #
150
+ # @param [Scalar value] partition key
151
+ # @param [Scalar value] sort key (optional)
152
+ # @param [Hash] attributes
153
+ # @param [Hash] conditions
154
+ #
155
+ # @return [Dynamoid::Document|nil] updated document
156
+ #
157
+ # @example Update document
158
+ # Post.update_fields(101, read: true)
159
+ #
160
+ # @example Update document with condition
161
+ # Post.update_fields(101, { title: 'New title' }, if: { version: 1 })
162
+ def update_fields(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
163
+ optional_params = [range_key_value, attrs, conditions].compact
164
+ if optional_params.first.is_a?(Hash)
165
+ range_key_value = nil
166
+ attrs, conditions = optional_params[0..1]
108
167
  else
109
- array = documents.map do |d|
110
- Dumping.dump_attributes(d.attributes, attributes)
111
- end
168
+ range_key_value = optional_params.first
169
+ attrs, conditions = optional_params[1..2]
170
+ end
112
171
 
113
- Dynamoid.adapter.batch_write_item(table_name, array)
172
+ UpdateFields.call(self,
173
+ partition_key: hash_key_value,
174
+ sort_key: range_key_value,
175
+ attributes: attrs,
176
+ conditions: conditions)
177
+ end
178
+
179
+ # Update existing document or create new one.
180
+ #
181
+ # Similar to `.update_fields`.
182
+ # The only diffirence is - it creates new document in case the document doesn't exist.
183
+ #
184
+ # Uses efficient low-level `UpdateItem` API call.
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`.
188
+ #
189
+ # @param [Scalar value] partition key
190
+ # @param [Scalar value] sort key (optional)
191
+ # @param [Hash] attributes
192
+ # @param [Hash] conditions
193
+ #
194
+ # @return [Dynamoid::Document/nil] updated document
195
+ #
196
+ # @example Update document
197
+ # Post.upsert(101, title: 'New title')
198
+ def upsert(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
199
+ optional_params = [range_key_value, attrs, conditions].compact
200
+ if optional_params.first.is_a?(Hash)
201
+ range_key_value = nil
202
+ attrs, conditions = optional_params[0..1]
203
+ else
204
+ range_key_value = optional_params.first
205
+ attrs, conditions = optional_params[1..2]
114
206
  end
115
207
 
116
- documents.each { |d| d.new_record = false }
117
- documents
208
+ Upsert.call(self,
209
+ partition_key: hash_key_value,
210
+ sort_key: range_key_value,
211
+ attributes: attrs,
212
+ conditions: conditions)
213
+ end
214
+
215
+ # Increase numeric field by specified value.
216
+ #
217
+ # Can update several fields at once.
218
+ # Uses efficient low-level `UpdateItem` API call.
219
+ #
220
+ # @param [Scalar value] hash_key_value partition key
221
+ # @param [Scalar value] range_key_value sort key (optional)
222
+ # @param [Hash] counters value to increase by
223
+ #
224
+ # @return [Dynamoid::Document/nil] updated document
225
+ #
226
+ # @example Update document
227
+ # Post.inc(101, views_counter: 2, downloads: 10)
228
+ def inc(hash_key_value, range_key_value = nil, counters)
229
+ options = if range_key
230
+ value_casted = TypeCasting.cast_field(range_key_value, attributes[range_key])
231
+ value_dumped = Dumping.dump_field(value_casted, attributes[range_key])
232
+ { range_key: value_dumped }
233
+ else
234
+ {}
235
+ end
236
+
237
+ Dynamoid.adapter.update_item(table_name, hash_key_value, options) do |t|
238
+ counters.each do |k, v|
239
+ value_casted = TypeCasting.cast_field(v, attributes[k])
240
+ value_dumped = Dumping.dump_field(value_casted, attributes[k])
241
+
242
+ t.add(k => value_dumped)
243
+ end
244
+ end
118
245
  end
119
246
  end
120
247
 
@@ -141,12 +268,15 @@ module Dynamoid
141
268
  self.class.create_table
142
269
 
143
270
  if new_record?
144
- conditions = { unless_exists: [self.class.hash_key] }
145
- conditions[:unless_exists] << range_key if range_key
146
-
147
- run_callbacks(:create) { persist(conditions) }
271
+ run_callbacks(:create) do
272
+ run_callbacks(:save) do
273
+ Save.call(self)
274
+ end
275
+ end
148
276
  else
149
- persist
277
+ run_callbacks(:save) do
278
+ Save.call(self)
279
+ end
150
280
  end
151
281
  end
152
282
 
@@ -206,6 +336,7 @@ module Dynamoid
206
336
  raise Dynamoid::Errors::StaleObjectError.new(self, 'update')
207
337
  end
208
338
  end
339
+
209
340
  end
210
341
 
211
342
  def update(conditions = {}, &block)
@@ -280,44 +411,5 @@ module Dynamoid
280
411
  rescue Dynamoid::Errors::ConditionalCheckFailedException
281
412
  raise Dynamoid::Errors::StaleObjectError.new(self, 'delete')
282
413
  end
283
-
284
- private
285
-
286
- # Persist the object into the datastore. Assign it an id first if it doesn't have one.
287
- #
288
- # @since 0.2.0
289
- def persist(conditions = nil)
290
- run_callbacks(:save) do
291
- self.hash_key = SecureRandom.uuid if hash_key.blank?
292
-
293
- # Add an exists check to prevent overwriting existing records with new ones
294
- if new_record?
295
- conditions ||= {}
296
- (conditions[:unless_exists] ||= []) << self.class.hash_key
297
- end
298
-
299
- # Add an optimistic locking check if the lock_version column exists
300
- if self.class.attributes[:lock_version]
301
- conditions ||= {}
302
- self.lock_version = (lock_version || 0) + 1
303
- # Uses the original lock_version value from ActiveModel::Dirty in case user changed lock_version manually
304
- (conditions[:if] ||= {})[:lock_version] = changes[:lock_version][0] if changes[:lock_version][0]
305
- end
306
-
307
- attributes_dumped = Dumping.dump_attributes(attributes, self.class.attributes)
308
-
309
- begin
310
- Dynamoid.adapter.write(self.class.table_name, attributes_dumped, conditions)
311
- @new_record = false
312
- true
313
- rescue Dynamoid::Errors::ConditionalCheckFailedException => e
314
- if new_record?
315
- raise Dynamoid::Errors::RecordNotUnique.new(e, self)
316
- else
317
- raise Dynamoid::Errors::StaleObjectError.new(self, 'persist')
318
- end
319
- end
320
- end
321
- end
322
414
  end
323
415
  end