dynamoid 3.10.0 → 3.11.0

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