dynamoid 3.10.0 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/README.md +182 -2
  4. data/dynamoid.gemspec +4 -4
  5. data/lib/dynamoid/adapter.rb +1 -1
  6. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/filter_expression_convertor.rb +53 -18
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +5 -4
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/projection_expression_convertor.rb +9 -7
  9. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +1 -1
  10. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +1 -1
  11. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/transact.rb +31 -0
  12. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +9 -5
  13. data/lib/dynamoid/components.rb +1 -0
  14. data/lib/dynamoid/config.rb +2 -0
  15. data/lib/dynamoid/criteria/chain.rb +63 -18
  16. data/lib/dynamoid/criteria/where_conditions.rb +13 -6
  17. data/lib/dynamoid/dirty.rb +86 -11
  18. data/lib/dynamoid/dumping.rb +36 -14
  19. data/lib/dynamoid/errors.rb +14 -2
  20. data/lib/dynamoid/finders.rb +6 -6
  21. data/lib/dynamoid/loadable.rb +1 -0
  22. data/lib/dynamoid/persistence/inc.rb +6 -7
  23. data/lib/dynamoid/persistence/item_updater_with_casting_and_dumping.rb +36 -0
  24. data/lib/dynamoid/persistence/item_updater_with_dumping.rb +33 -0
  25. data/lib/dynamoid/persistence/save.rb +5 -2
  26. data/lib/dynamoid/persistence/update_fields.rb +5 -3
  27. data/lib/dynamoid/persistence/upsert.rb +5 -4
  28. data/lib/dynamoid/persistence.rb +38 -17
  29. data/lib/dynamoid/transaction_write/base.rb +47 -0
  30. data/lib/dynamoid/transaction_write/create.rb +49 -0
  31. data/lib/dynamoid/transaction_write/delete_with_instance.rb +60 -0
  32. data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +59 -0
  33. data/lib/dynamoid/transaction_write/destroy.rb +79 -0
  34. data/lib/dynamoid/transaction_write/save.rb +164 -0
  35. data/lib/dynamoid/transaction_write/update_attributes.rb +46 -0
  36. data/lib/dynamoid/transaction_write/update_fields.rb +102 -0
  37. data/lib/dynamoid/transaction_write/upsert.rb +96 -0
  38. data/lib/dynamoid/transaction_write.rb +464 -0
  39. data/lib/dynamoid/type_casting.rb +3 -1
  40. data/lib/dynamoid/undumping.rb +13 -2
  41. data/lib/dynamoid/validations.rb +1 -1
  42. data/lib/dynamoid/version.rb +1 -1
  43. data/lib/dynamoid.rb +7 -0
  44. metadata +18 -5
@@ -95,20 +95,27 @@ module Dynamoid
95
95
  #
96
96
  # Internally +where+ performs either +Scan+ or +Query+ operation.
97
97
  #
98
+ # Conditions can be specified as an expression as well:
99
+ #
100
+ # Post.where('links_count = :v', v: 2)
101
+ #
102
+ # This way complex expressions can be constructed (e.g. with AND, OR, and NOT
103
+ # keyword):
104
+ #
105
+ # Address.where('city = :c AND (post_code = :pc1 OR post_code = :pc2)', city: 'A', pc1: '001', pc2: '002')
106
+ #
107
+ # See documentation for condition expression's syntax and examples:
108
+ # - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html
109
+ # - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.FilterExpression.html
110
+ #
98
111
  # @return [Dynamoid::Criteria::Chain]
99
112
  # @since 0.2.0
100
- def where(args)
101
- detector = NonexistentFieldsDetector.new(args, @source)
102
- if detector.found?
103
- Dynamoid.logger.warn(detector.warning_message)
113
+ def where(conditions, placeholders = nil)
114
+ if conditions.is_a?(Hash)
115
+ where_with_hash(conditions)
116
+ else
117
+ where_with_string(conditions, placeholders)
104
118
  end
105
-
106
- @where_conditions.update(args.symbolize_keys)
107
-
108
- # we should re-initialize keys detector every time we change @where_conditions
109
- @key_fields_detector = KeyFieldsDetector.new(@where_conditions, @source, forced_index_name: @forced_index_name)
110
-
111
- self
112
119
  end
113
120
 
114
121
  # Turns on strongly consistent reads.
@@ -500,6 +507,29 @@ module Dynamoid
500
507
 
501
508
  private
502
509
 
510
+ def where_with_hash(conditions)
511
+ detector = NonexistentFieldsDetector.new(conditions, @source)
512
+ if detector.found?
513
+ Dynamoid.logger.warn(detector.warning_message)
514
+ end
515
+
516
+ @where_conditions.update_with_hash(conditions.symbolize_keys)
517
+
518
+ # we should re-initialize keys detector every time we change @where_conditions
519
+ @key_fields_detector = KeyFieldsDetector.new(@where_conditions, @source, forced_index_name: @forced_index_name)
520
+
521
+ self
522
+ end
523
+
524
+ def where_with_string(query, placeholders)
525
+ @where_conditions.update_with_string(query, placeholders)
526
+
527
+ # we should re-initialize keys detector every time we change @where_conditions
528
+ @key_fields_detector = KeyFieldsDetector.new(@where_conditions, @source, forced_index_name: @forced_index_name)
529
+
530
+ self
531
+ end
532
+
503
533
  # The actual records referenced by the association.
504
534
  #
505
535
  # @return [Enumerator] an iterator of the found records.
@@ -635,12 +665,12 @@ module Dynamoid
635
665
  end
636
666
 
637
667
  def query_non_key_conditions
638
- opts = {}
668
+ hash_conditions = {}
639
669
 
640
670
  # Honor STI and :type field if it presents
641
671
  if @source.attributes.key?(@source.inheritance_field) &&
642
672
  @key_fields_detector.hash_key.to_sym != @source.inheritance_field.to_sym
643
- @where_conditions.update(sti_condition)
673
+ @where_conditions.update_with_hash(sti_condition)
644
674
  end
645
675
 
646
676
  # TODO: Separate key conditions and non-key conditions properly:
@@ -650,11 +680,17 @@ module Dynamoid
650
680
  .reject { |k, _| k.to_s =~ /^#{@key_fields_detector.range_key}\./ }
651
681
  keys.each do |key|
652
682
  name, condition = field_condition(key, @where_conditions[key])
653
- opts[name] ||= []
654
- opts[name] << condition
683
+ hash_conditions[name] ||= []
684
+ hash_conditions[name] << condition
655
685
  end
656
686
 
657
- opts
687
+ string_conditions = []
688
+ @where_conditions.string_conditions.each do |query, placeholders|
689
+ placeholders ||= {}
690
+ string_conditions << [query, placeholders]
691
+ end
692
+
693
+ [hash_conditions] + string_conditions
658
694
  end
659
695
 
660
696
  # TODO: casting should be operator aware
@@ -721,16 +757,25 @@ module Dynamoid
721
757
  def scan_conditions
722
758
  # Honor STI and :type field if it presents
723
759
  if sti_condition
724
- @where_conditions.update(sti_condition)
760
+ @where_conditions.update_with_hash(sti_condition)
725
761
  end
726
762
 
727
- {}.tap do |opts|
763
+ hash_conditions = {}
764
+ hash_conditions.tap do |opts|
728
765
  @where_conditions.keys.map(&:to_sym).each do |key|
729
766
  name, condition = field_condition(key, @where_conditions[key])
730
767
  opts[name] ||= []
731
768
  opts[name] << condition
732
769
  end
733
770
  end
771
+
772
+ string_conditions = []
773
+ @where_conditions.string_conditions.each do |query, placeholders|
774
+ placeholders ||= {}
775
+ string_conditions << [query, placeholders]
776
+ end
777
+
778
+ [hash_conditions] + string_conditions
734
779
  end
735
780
 
736
781
  def scan_options
@@ -4,24 +4,31 @@ module Dynamoid
4
4
  module Criteria
5
5
  # @private
6
6
  class WhereConditions
7
+ attr_reader :string_conditions
8
+
7
9
  def initialize
8
- @conditions = []
10
+ @hash_conditions = []
11
+ @string_conditions = []
12
+ end
13
+
14
+ def update_with_hash(hash)
15
+ @hash_conditions << hash.symbolize_keys
9
16
  end
10
17
 
11
- def update(hash)
12
- @conditions << hash.symbolize_keys
18
+ def update_with_string(query, placeholders)
19
+ @string_conditions << [query, placeholders]
13
20
  end
14
21
 
15
22
  def keys
16
- @conditions.flat_map(&:keys)
23
+ @hash_conditions.flat_map(&:keys)
17
24
  end
18
25
 
19
26
  def empty?
20
- @conditions.empty?
27
+ @hash_conditions.empty? && @string_conditions.empty?
21
28
  end
22
29
 
23
30
  def [](key)
24
- hash = @conditions.find { |h| h.key?(key) }
31
+ hash = @hash_conditions.find { |h| h.key?(key) }
25
32
  hash[key] if hash
26
33
  end
27
34
  end
@@ -36,8 +36,11 @@ module Dynamoid
36
36
  end
37
37
  end
38
38
 
39
- def from_database(*)
40
- super.tap(&:clear_changes_information)
39
+ def from_database(attributes_from_database)
40
+ super.tap do |model|
41
+ model.clear_changes_information
42
+ model.assign_attributes_from_database(DeepDupper.dup_attributes(model.attributes, model.class))
43
+ end
41
44
  end
42
45
  end
43
46
 
@@ -108,7 +111,7 @@ module Dynamoid
108
111
  #
109
112
  # @return [ActiveSupport::HashWithIndifferentAccess]
110
113
  def changes
111
- ActiveSupport::HashWithIndifferentAccess[changed.map { |name| [name, attribute_change(name)] }]
114
+ ActiveSupport::HashWithIndifferentAccess[changed_attributes.map { |name, old_value| [name, [old_value, read_attribute(name)]] }]
112
115
  end
113
116
 
114
117
  # Returns a hash of attributes that were changed before the model was saved.
@@ -132,19 +135,21 @@ module Dynamoid
132
135
  #
133
136
  # @return [ActiveSupport::HashWithIndifferentAccess]
134
137
  def changed_attributes
135
- @changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
138
+ attributes_changed_by_setter.merge(attributes_changed_in_place)
136
139
  end
137
140
 
138
141
  # Clear all dirty data: current changes and previous changes.
139
142
  def clear_changes_information
140
143
  @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
141
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
144
+ @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
145
+ @attributes_from_database = HashWithIndifferentAccess.new(DeepDupper.dup_attributes(@attributes, self.class))
142
146
  end
143
147
 
144
148
  # Clears dirty data and moves +changes+ to +previous_changes+.
145
149
  def changes_applied
146
150
  @previously_changed = changes
147
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
151
+ @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
152
+ @attributes_from_database = HashWithIndifferentAccess.new(DeepDupper.dup_attributes(@attributes, self.class))
148
153
  end
149
154
 
150
155
  # Remove changes information for the provided attributes.
@@ -152,6 +157,9 @@ module Dynamoid
152
157
  # @param attributes [Array[String]] - a list of attributes to clear changes for
153
158
  def clear_attribute_changes(names)
154
159
  attributes_changed_by_setter.except!(*names)
160
+
161
+ slice = HashWithIndifferentAccess.new(@attributes).slice(*names)
162
+ attributes_from_database.merge!(DeepDupper.dup_attributes(slice, self.class))
155
163
  end
156
164
 
157
165
  # Handle <tt>*_changed?</tt> for +method_missing+.
@@ -220,12 +228,16 @@ module Dynamoid
220
228
  previous_changes[name] if attribute_previously_changed?(name)
221
229
  end
222
230
 
231
+ # @private
232
+ def assign_attributes_from_database(attributes_from_database)
233
+ @attributes_from_database = HashWithIndifferentAccess.new(attributes_from_database)
234
+ end
235
+
223
236
  private
224
237
 
225
238
  def changes_include?(name)
226
- attributes_changed_by_setter.include?(name)
239
+ attribute_changed_by_setter?(name) || attribute_changed_in_place?(name)
227
240
  end
228
- alias attribute_changed_by_setter? changes_include?
229
241
 
230
242
  # Handle <tt>*_change</tt> for +method_missing+.
231
243
  def attribute_change(name)
@@ -259,13 +271,76 @@ module Dynamoid
259
271
  previous_changes.include?(name)
260
272
  end
261
273
 
262
- # This is necessary because `changed_attributes` might be overridden in
263
- # other implemntations (e.g. in `ActiveRecord`)
264
- alias attributes_changed_by_setter changed_attributes
274
+ def attributes_changed_by_setter
275
+ @attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new
276
+ end
277
+
278
+ def attribute_changed_by_setter?(name)
279
+ attributes_changed_by_setter.include?(name)
280
+ end
281
+
282
+ def attributes_from_database
283
+ @attributes_from_database ||= ActiveSupport::HashWithIndifferentAccess.new
284
+ end
265
285
 
266
286
  # Force an attribute to have a particular "before" value
267
287
  def set_attribute_was(name, old_value)
268
288
  attributes_changed_by_setter[name] = old_value
269
289
  end
290
+
291
+ def attributes_changed_in_place
292
+ attributes_from_database.select do |name, _|
293
+ attribute_changed_in_place?(name)
294
+ end
295
+ end
296
+
297
+ def attribute_changed_in_place?(name)
298
+ return false if attribute_changed_by_setter?(name)
299
+
300
+ value_from_database = attributes_from_database[name]
301
+ return false if value_from_database.nil?
302
+
303
+ value = read_attribute(name)
304
+ value != value_from_database
305
+ end
306
+
307
+ module DeepDupper
308
+ def self.dup_attributes(attributes, klass)
309
+ attributes.map do |name, value|
310
+ type_options = klass.attributes[name.to_sym]
311
+ value_duplicate = dup_attribute(value, type_options)
312
+ [name, value_duplicate]
313
+ end.to_h
314
+ end
315
+
316
+ def self.dup_attribute(value, type_options)
317
+ type, of = type_options.values_at(:type, :of)
318
+
319
+ case value
320
+ when NilClass, TrueClass, FalseClass, Numeric, Symbol, IO
321
+ # till Ruby 2.4 these immutable objects could not be duplicated
322
+ # IO objects cannot be duplicated - is used for binary fields
323
+ value
324
+ when String
325
+ value.dup
326
+ when Array
327
+ if of.is_a? Class # custom type
328
+ value.map { |e| dup_attribute(e, type: of) }
329
+ else
330
+ value.deep_dup
331
+ end
332
+ when Set
333
+ Set.new(value.map { |e| dup_attribute(e, type: of) })
334
+ when Hash
335
+ value.deep_dup
336
+ else
337
+ if type.is_a? Class # custom type
338
+ Marshal.load(Marshal.dump(value)) # dup instance variables
339
+ else
340
+ value.dup # date, datetime
341
+ end
342
+ end
343
+ end
344
+ end
270
345
  end
271
346
  end
@@ -70,7 +70,8 @@ module Dynamoid
70
70
  end
71
71
 
72
72
  def invalid_value?(value)
73
- (value.is_a?(Set) || value.is_a?(String)) && value.empty?
73
+ (value.is_a?(Set) && value.empty?) ||
74
+ (value.is_a?(String) && value.empty? && Config.store_empty_string_as_nil)
74
75
  end
75
76
  end
76
77
 
@@ -86,6 +87,12 @@ module Dynamoid
86
87
 
87
88
  # string -> string
88
89
  class StringDumper < Base
90
+ def process(string)
91
+ return nil if string.nil?
92
+ return nil if string.empty? && Config.store_empty_string_as_nil
93
+
94
+ string
95
+ end
89
96
  end
90
97
 
91
98
  # integer -> number
@@ -101,6 +108,8 @@ module Dynamoid
101
108
  ALLOWED_TYPES = %i[string integer number date datetime serialized].freeze
102
109
 
103
110
  def process(set)
111
+ return nil if set.is_a?(Set) && set.empty?
112
+
104
113
  if @options.key?(:of)
105
114
  process_typed_collection(set)
106
115
  else
@@ -112,13 +121,13 @@ module Dynamoid
112
121
 
113
122
  def process_typed_collection(set)
114
123
  if allowed_type?
115
- dumper = Dumping.find_dumper(element_options)
116
- result = set.map { |el| dumper.process(el) }
117
-
118
- if element_type == :string
119
- result.reject!(&:empty?)
124
+ # StringDumper may replace "" with nil so we cannot distinguish it from an explicit nil
125
+ if element_type == :string && Config.store_empty_string_as_nil
126
+ set.reject! { |s| s && s.empty? }
120
127
  end
121
128
 
129
+ dumper = Dumping.find_dumper(element_options)
130
+ result = set.map { |el| dumper.process(el) }
122
131
  result.to_set
123
132
  else
124
133
  raise ArgumentError, "Set element type #{element_type} isn't supported"
@@ -164,14 +173,13 @@ module Dynamoid
164
173
 
165
174
  def process_typed_collection(array)
166
175
  if allowed_type?
167
- dumper = Dumping.find_dumper(element_options)
168
- result = array.map { |el| dumper.process(el) }
169
-
170
- if element_type == :string
171
- result.reject!(&:empty?)
176
+ # StringDumper may replace "" with nil so we cannot distinguish it from an explicit nil
177
+ if element_type == :string && Config.store_empty_string_as_nil
178
+ array.reject! { |s| s && s.empty? }
172
179
  end
173
180
 
174
- result
181
+ dumper = Dumping.find_dumper(element_options)
182
+ array.map { |el| dumper.process(el) }
175
183
  else
176
184
  raise ArgumentError, "Array element type #{element_type} isn't supported"
177
185
  end
@@ -289,10 +297,24 @@ module Dynamoid
289
297
  end
290
298
  end
291
299
 
292
- # string -> string
300
+ # string -> StringIO
293
301
  class BinaryDumper < Base
294
302
  def process(value)
295
- Base64.strict_encode64(value)
303
+ store_as_binary = if @options[:store_as_native_binary].nil?
304
+ Dynamoid.config.store_binary_as_native
305
+ else
306
+ @options[:store_as_native_binary]
307
+ end
308
+
309
+ if store_as_binary
310
+ if value.is_a?(StringIO) || value.is_a?(IO)
311
+ value
312
+ else
313
+ StringIO.new(value)
314
+ end
315
+ else
316
+ Base64.strict_encode64(value)
317
+ end
296
318
  end
297
319
  end
298
320
 
@@ -6,6 +6,7 @@ module Dynamoid
6
6
  # Generic Dynamoid error
7
7
  class Error < StandardError; end
8
8
 
9
+ class MissingHashKey < Error; end
9
10
  class MissingRangeKey < Error; end
10
11
 
11
12
  class MissingIndex < Error; end
@@ -15,18 +16,27 @@ module Dynamoid
15
16
  class InvalidIndex < Error
16
17
  def initialize(item)
17
18
  if item.is_a? String
18
- super(item)
19
+ super
19
20
  else
20
21
  super("Validation failed: #{item.errors.full_messages.join(', ')}")
21
22
  end
22
23
  end
23
24
  end
24
25
 
26
+ class RecordNotSaved < Error
27
+ attr_reader :record
28
+
29
+ def initialize(record)
30
+ super('Failed to save the item')
31
+ @record = record
32
+ end
33
+ end
34
+
25
35
  class RecordNotDestroyed < Error
26
36
  attr_reader :record
27
37
 
28
38
  def initialize(record)
29
- super('Failed to destroy item')
39
+ super('Failed to destroy the item')
30
40
  @record = record
31
41
  end
32
42
  end
@@ -79,5 +89,7 @@ module Dynamoid
79
89
  class UnknownAttribute < Error; end
80
90
 
81
91
  class SubclassNotFound < Error; end
92
+
93
+ class Rollback < Error; end
82
94
  end
83
95
  end
@@ -78,7 +78,7 @@ module Dynamoid
78
78
  # # Find all the tweets using hash key and range key with consistent read
79
79
  # Tweet.find_all([['1', 'red'], ['1', 'green']], consistent_read: true)
80
80
  def find_all(ids, options = {})
81
- ActiveSupport::Deprecation.warn('[Dynamoid] .find_all is deprecated! Call .find instead of')
81
+ Dynamoid.deprecator.warn('[Dynamoid] .find_all is deprecated! Call .find instead of')
82
82
 
83
83
  _find_all(ids, options)
84
84
  end
@@ -100,7 +100,7 @@ module Dynamoid
100
100
  #
101
101
  # @since 0.2.0
102
102
  def find_by_id(id, options = {})
103
- ActiveSupport::Deprecation.warn('[Dynamoid] .find_by_id is deprecated! Call .find instead of', caller[1..-1])
103
+ Dynamoid.deprecator.warn('[Dynamoid] .find_by_id is deprecated! Call .find instead of')
104
104
 
105
105
  _find_by_id(id, options)
106
106
  end
@@ -180,7 +180,7 @@ module Dynamoid
180
180
  # @param range_key [Scalar value] range key of the object to find
181
181
  #
182
182
  def find_by_composite_key(hash_key, range_key, options = {})
183
- ActiveSupport::Deprecation.warn('[Dynamoid] .find_by_composite_key is deprecated! Call .find instead of')
183
+ Dynamoid.deprecator.warn('[Dynamoid] .find_by_composite_key is deprecated! Call .find instead of')
184
184
 
185
185
  _find_by_id(hash_key, options.merge(range_key: range_key))
186
186
  end
@@ -207,7 +207,7 @@ module Dynamoid
207
207
  #
208
208
  # @return [Array] an array of all matching items
209
209
  def find_all_by_composite_key(hash_key, options = {})
210
- ActiveSupport::Deprecation.warn('[Dynamoid] .find_all_composite_key is deprecated! Call .where instead of')
210
+ Dynamoid.deprecator.warn('[Dynamoid] .find_all_composite_key is deprecated! Call .where instead of')
211
211
 
212
212
  Dynamoid.adapter.query(table_name, options.merge(hash_value: hash_key)).flat_map { |i| i }.collect do |item|
213
213
  from_database(item)
@@ -237,7 +237,7 @@ module Dynamoid
237
237
  # @param options [Hash] conditions on range key e.g. +{ "rank.lte": 10 }, query filter, projected keys, scan_index_forward etc.
238
238
  # @return [Array] an array of all matching items
239
239
  def find_all_by_secondary_index(hash, options = {})
240
- ActiveSupport::Deprecation.warn('[Dynamoid] .find_all_by_secondary_index is deprecated! Call .where instead of')
240
+ Dynamoid.deprecator.warn('[Dynamoid] .find_all_by_secondary_index is deprecated! Call .where instead of')
241
241
 
242
242
  range = options[:range] || {}
243
243
  hash_key_field, hash_key_value = hash.first
@@ -291,7 +291,7 @@ module Dynamoid
291
291
  def method_missing(method, *args)
292
292
  # Cannot use Symbol#start_with? because it was introduced in Ruby 2.7, but we support Ruby >= 2.3
293
293
  if method.to_s.start_with?('find')
294
- ActiveSupport::Deprecation.warn("[Dynamoid] .#{method} is deprecated! Call .where instead of")
294
+ Dynamoid.deprecator.warn("[Dynamoid] .#{method} is deprecated! Call .where instead of")
295
295
 
296
296
  finder = method.to_s.split('_by_').first
297
297
  attributes = method.to_s.split('_by_').last.split('_and_')
@@ -11,6 +11,7 @@ module Dynamoid
11
11
 
12
12
  self
13
13
  end
14
+ alias assign_attributes load
14
15
 
15
16
  # Reload an object from the database -- if you suspect the object has changed in the data store and you need those
16
17
  # changes to be reflected immediately, you would call this method. This is a consistent read.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'item_updater_with_casting_and_dumping'
4
+
3
5
  module Dynamoid
4
6
  module Persistence
5
7
  # @private
@@ -21,15 +23,17 @@ module Dynamoid
21
23
  touch = @counters.delete(:touch)
22
24
 
23
25
  Dynamoid.adapter.update_item(@model_class.table_name, @hash_key, update_item_options) do |t|
26
+ item_updater = ItemUpdaterWithCastingAndDumping.new(@model_class, t)
27
+
24
28
  @counters.each do |name, value|
25
- t.add(name => cast_and_dump_attribute_value(name, value))
29
+ item_updater.add(name => value)
26
30
  end
27
31
 
28
32
  if touch
29
33
  value = DateTime.now.in_time_zone(Time.zone)
30
34
 
31
35
  timestamp_attributes_to_touch(touch).each do |name|
32
- t.set(name => cast_and_dump_attribute_value(name, value))
36
+ item_updater.set(name => value)
33
37
  end
34
38
  end
35
39
  end
@@ -48,11 +52,6 @@ module Dynamoid
48
52
  end
49
53
  end
50
54
 
51
- def cast_and_dump_attribute_value(name, value)
52
- value_casted = TypeCasting.cast_field(value, @model_class.attributes[name])
53
- Dumping.dump_field(value_casted, @model_class.attributes[name])
54
- end
55
-
56
55
  def timestamp_attributes_to_touch(touch)
57
56
  return [] unless touch
58
57
 
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module Persistence
5
+ # @private
6
+ class ItemUpdaterWithCastingAndDumping
7
+ def initialize(model_class, item_updater)
8
+ @model_class = model_class
9
+ @item_updater = item_updater
10
+ end
11
+
12
+ def add(attributes)
13
+ @item_updater.add(cast_and_dump(attributes))
14
+ end
15
+
16
+ def set(attributes)
17
+ @item_updater.set(cast_and_dump(attributes))
18
+ end
19
+
20
+ private
21
+
22
+ def cast_and_dump(attributes)
23
+ casted_and_dumped = {}
24
+
25
+ attributes.each do |name, value|
26
+ value_casted = TypeCasting.cast_field(value, @model_class.attributes[name])
27
+ value_dumped = Dumping.dump_field(value_casted, @model_class.attributes[name])
28
+
29
+ casted_and_dumped[name] = value_dumped
30
+ end
31
+
32
+ casted_and_dumped
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module Persistence
5
+ # @private
6
+ class ItemUpdaterWithDumping
7
+ def initialize(model_class, item_updater)
8
+ @model_class = model_class
9
+ @item_updater = item_updater
10
+ end
11
+
12
+ def add(attributes)
13
+ @item_updater.add(dump(attributes))
14
+ end
15
+
16
+ def set(attributes)
17
+ @item_updater.set(dump(attributes))
18
+ end
19
+
20
+ private
21
+
22
+ def dump(attributes)
23
+ dumped = {}
24
+
25
+ attributes.each do |name, value|
26
+ dumped[name] = Dumping.dump_field(value, @model_class.attributes[name])
27
+ end
28
+
29
+ dumped
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'item_updater_with_dumping'
4
+
3
5
  module Dynamoid
4
6
  module Persistence
5
7
  # @private
@@ -36,9 +38,10 @@ module Dynamoid
36
38
  attributes_to_persist = @model.attributes.slice(*@model.changed.map(&:to_sym))
37
39
 
38
40
  Dynamoid.adapter.update_item(@model.class.table_name, @model.hash_key, options_to_update_item) do |t|
41
+ item_updater = ItemUpdaterWithDumping.new(@model.class, t)
42
+
39
43
  attributes_to_persist.each do |name, value|
40
- value_dumped = Dumping.dump_field(value, @model.class.attributes[name])
41
- t.set(name => value_dumped)
44
+ item_updater.set(name => value)
42
45
  end
43
46
  end
44
47
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'item_updater_with_casting_and_dumping'
4
+
3
5
  module Dynamoid
4
6
  module Persistence
5
7
  # @private
@@ -32,10 +34,10 @@ module Dynamoid
32
34
 
33
35
  def update_item
34
36
  Dynamoid.adapter.update_item(@model_class.table_name, @partition_key, options_to_update_item) do |t|
37
+ item_updater = ItemUpdaterWithCastingAndDumping.new(@model_class, t)
38
+
35
39
  @attributes.each do |k, v|
36
- value_casted = TypeCasting.cast_field(v, @model_class.attributes[k])
37
- value_dumped = Dumping.dump_field(value_casted, @model_class.attributes[k])
38
- t.set(k => value_dumped)
40
+ item_updater.set(k => v)
39
41
  end
40
42
  end
41
43
  end