dynamoid 3.2.0 → 3.3.0

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