dynamoid 3.11.0 → 3.12.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -3
- data/README.md +93 -13
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +8 -0
- data/lib/dynamoid/config.rb +1 -0
- data/lib/dynamoid/criteria/chain.rb +11 -3
- data/lib/dynamoid/dirty.rb +22 -11
- data/lib/dynamoid/dumping.rb +3 -3
- data/lib/dynamoid/errors.rb +16 -1
- data/lib/dynamoid/fields.rb +13 -3
- data/lib/dynamoid/finders.rb +38 -17
- data/lib/dynamoid/persistence/inc.rb +30 -13
- data/lib/dynamoid/persistence/save.rb +24 -12
- data/lib/dynamoid/persistence/update_fields.rb +18 -5
- data/lib/dynamoid/persistence/update_validations.rb +3 -3
- data/lib/dynamoid/persistence/upsert.rb +17 -4
- data/lib/dynamoid/persistence.rb +146 -11
- data/lib/dynamoid/transaction_read/find.rb +137 -0
- data/lib/dynamoid/transaction_read.rb +146 -0
- data/lib/dynamoid/transaction_write/delete_with_instance.rb +7 -2
- data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +7 -2
- data/lib/dynamoid/transaction_write/destroy.rb +7 -2
- data/lib/dynamoid/transaction_write/item_updater.rb +55 -0
- data/lib/dynamoid/transaction_write/save.rb +7 -2
- data/lib/dynamoid/transaction_write/update_fields.rb +169 -32
- data/lib/dynamoid/transaction_write/upsert.rb +12 -2
- data/lib/dynamoid/transaction_write.rb +212 -3
- data/lib/dynamoid/validations.rb +7 -4
- data/lib/dynamoid/version.rb +1 -1
- data/lib/dynamoid.rb +1 -0
- metadata +8 -5
@@ -12,10 +12,12 @@ module Dynamoid
|
|
12
12
|
|
13
13
|
def initialize(model, touch: nil)
|
14
14
|
@model = model
|
15
|
-
@touch = touch # touch
|
15
|
+
@touch = touch # `touch: false` means explicit disabling of updating the `updated_at` attribute
|
16
16
|
end
|
17
17
|
|
18
18
|
def call
|
19
|
+
validate_primary_key!
|
20
|
+
|
19
21
|
@model.hash_key = SecureRandom.uuid if @model.hash_key.blank?
|
20
22
|
|
21
23
|
return true unless @model.changed?
|
@@ -36,8 +38,10 @@ module Dynamoid
|
|
36
38
|
Dynamoid.adapter.write(@model.class.table_name, attributes_dumped, conditions_for_write)
|
37
39
|
else
|
38
40
|
attributes_to_persist = @model.attributes.slice(*@model.changed.map(&:to_sym))
|
41
|
+
partition_key_dumped = dump(@model.class.hash_key, @model.hash_key)
|
42
|
+
options = options_to_update_item(partition_key_dumped)
|
39
43
|
|
40
|
-
Dynamoid.adapter.update_item(@model.class.table_name,
|
44
|
+
Dynamoid.adapter.update_item(@model.class.table_name, partition_key_dumped, options) do |t|
|
41
45
|
item_updater = ItemUpdaterWithDumping.new(@model.class, t)
|
42
46
|
|
43
47
|
attributes_to_persist.each do |name, value|
|
@@ -58,22 +62,25 @@ module Dynamoid
|
|
58
62
|
|
59
63
|
private
|
60
64
|
|
65
|
+
def validate_primary_key!
|
66
|
+
raise Dynamoid::Errors::MissingHashKey if !@model.new_record? && @model.hash_key.nil?
|
67
|
+
raise Dynamoid::Errors::MissingRangeKey if @model.class.range_key? && @model.range_value.nil?
|
68
|
+
end
|
69
|
+
|
61
70
|
# Should be called after incrementing `lock_version` attribute
|
62
71
|
def conditions_for_write
|
63
72
|
conditions = {}
|
64
73
|
|
65
74
|
# Add an 'exists' check to prevent overwriting existing records with new ones
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
conditions[:unless_exists] << @model.range_key
|
70
|
-
end
|
75
|
+
conditions[:unless_exists] = [@model.class.hash_key]
|
76
|
+
if @model.range_key
|
77
|
+
conditions[:unless_exists] << @model.range_key
|
71
78
|
end
|
72
79
|
|
73
80
|
# Add an optimistic locking check if the lock_version column exists
|
74
81
|
# Uses the original lock_version value from Dirty API
|
75
82
|
# in case user changed 'lock_version' manually
|
76
|
-
if @model.class.attributes[:lock_version] &&
|
83
|
+
if @model.class.attributes[:lock_version] && @model.changes[:lock_version][0]
|
77
84
|
conditions[:if] ||= {}
|
78
85
|
conditions[:if][:lock_version] = @model.changes[:lock_version][0]
|
79
86
|
end
|
@@ -81,22 +88,22 @@ module Dynamoid
|
|
81
88
|
conditions
|
82
89
|
end
|
83
90
|
|
84
|
-
def options_to_update_item
|
91
|
+
def options_to_update_item(partition_key_dumped)
|
85
92
|
options = {}
|
86
93
|
|
87
94
|
if @model.class.range_key
|
88
|
-
value_dumped =
|
95
|
+
value_dumped = dump(@model.class.range_key, @model.range_value)
|
89
96
|
options[:range_key] = value_dumped
|
90
97
|
end
|
91
98
|
|
92
99
|
conditions = {}
|
93
100
|
conditions[:if] ||= {}
|
94
|
-
conditions[:if][@model.class.hash_key] =
|
101
|
+
conditions[:if][@model.class.hash_key] = partition_key_dumped
|
95
102
|
|
96
103
|
# Add an optimistic locking check if the lock_version column exists
|
97
104
|
# Uses the original lock_version value from Dirty API
|
98
105
|
# in case user changed 'lock_version' manually
|
99
|
-
if @model.class.attributes[:lock_version] &&
|
106
|
+
if @model.class.attributes[:lock_version] && @model.changes[:lock_version][0]
|
100
107
|
conditions[:if] ||= {}
|
101
108
|
conditions[:if][:lock_version] = @model.changes[:lock_version][0]
|
102
109
|
end
|
@@ -105,6 +112,11 @@ module Dynamoid
|
|
105
112
|
|
106
113
|
options
|
107
114
|
end
|
115
|
+
|
116
|
+
def dump(name, value)
|
117
|
+
options = @model.class.attributes[name]
|
118
|
+
Dumping.dump_field(value, options)
|
119
|
+
end
|
108
120
|
end
|
109
121
|
end
|
110
122
|
end
|
@@ -16,9 +16,12 @@ module Dynamoid
|
|
16
16
|
@sort_key = sort_key
|
17
17
|
@attributes = attributes.symbolize_keys
|
18
18
|
@conditions = conditions
|
19
|
+
|
20
|
+
@partition_key_dumped = cast_and_dump(@model_class.hash_key, @partition_key)
|
19
21
|
end
|
20
22
|
|
21
23
|
def call
|
24
|
+
validate_primary_key!
|
22
25
|
UpdateValidations.validate_attributes_exist(@model_class, @attributes)
|
23
26
|
|
24
27
|
if @model_class.timestamps_enabled?
|
@@ -32,8 +35,13 @@ module Dynamoid
|
|
32
35
|
|
33
36
|
private
|
34
37
|
|
38
|
+
def validate_primary_key!
|
39
|
+
raise Dynamoid::Errors::MissingHashKey if @partition_key.nil?
|
40
|
+
raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @sort_key.nil?
|
41
|
+
end
|
42
|
+
|
35
43
|
def update_item
|
36
|
-
Dynamoid.adapter.update_item(@model_class.table_name, @
|
44
|
+
Dynamoid.adapter.update_item(@model_class.table_name, @partition_key_dumped, options_to_update_item) do |t|
|
37
45
|
item_updater = ItemUpdaterWithCastingAndDumping.new(@model_class, t)
|
38
46
|
|
39
47
|
@attributes.each do |k, v|
|
@@ -50,18 +58,23 @@ module Dynamoid
|
|
50
58
|
options = {}
|
51
59
|
|
52
60
|
if @model_class.range_key
|
53
|
-
|
54
|
-
|
55
|
-
options[:range_key] = value_dumped
|
61
|
+
range_key_dumped = cast_and_dump(@model_class.range_key, @sort_key)
|
62
|
+
options[:range_key] = range_key_dumped
|
56
63
|
end
|
57
64
|
|
58
65
|
conditions = @conditions.deep_dup
|
59
66
|
conditions[:if] ||= {}
|
60
|
-
conditions[:if][@model_class.hash_key] = @
|
67
|
+
conditions[:if][@model_class.hash_key] = @partition_key_dumped
|
61
68
|
options[:conditions] = conditions
|
62
69
|
|
63
70
|
options
|
64
71
|
end
|
72
|
+
|
73
|
+
def cast_and_dump(name, value)
|
74
|
+
options = @model_class.attributes[name]
|
75
|
+
value_casted = TypeCasting.cast_field(value, options)
|
76
|
+
Dumping.dump_field(value_casted, options)
|
77
|
+
end
|
65
78
|
end
|
66
79
|
end
|
67
80
|
end
|
@@ -7,9 +7,9 @@ module Dynamoid
|
|
7
7
|
def self.validate_attributes_exist(model_class, attributes)
|
8
8
|
model_attributes = model_class.attributes.keys
|
9
9
|
|
10
|
-
attributes.each_key do |
|
11
|
-
unless model_attributes.include?(
|
12
|
-
raise Dynamoid::Errors::UnknownAttribute,
|
10
|
+
attributes.each_key do |name|
|
11
|
+
unless model_attributes.include?(name)
|
12
|
+
raise Dynamoid::Errors::UnknownAttribute.new(model_class, name)
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
@@ -19,6 +19,7 @@ module Dynamoid
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def call
|
22
|
+
validate_primary_key!
|
22
23
|
UpdateValidations.validate_attributes_exist(@model_class, @attributes)
|
23
24
|
|
24
25
|
if @model_class.timestamps_enabled?
|
@@ -32,8 +33,15 @@ module Dynamoid
|
|
32
33
|
|
33
34
|
private
|
34
35
|
|
36
|
+
def validate_primary_key!
|
37
|
+
raise Dynamoid::Errors::MissingHashKey if @partition_key.nil?
|
38
|
+
raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @sort_key.nil?
|
39
|
+
end
|
40
|
+
|
35
41
|
def update_item
|
36
|
-
|
42
|
+
partition_key_dumped = cast_and_dump(@model_class.hash_key, @partition_key)
|
43
|
+
|
44
|
+
Dynamoid.adapter.update_item(@model_class.table_name, partition_key_dumped, options_to_update_item) do |t|
|
37
45
|
item_updater = ItemUpdaterWithCastingAndDumping.new(@model_class, t)
|
38
46
|
|
39
47
|
@attributes.each do |k, v|
|
@@ -46,9 +54,8 @@ module Dynamoid
|
|
46
54
|
options = {}
|
47
55
|
|
48
56
|
if @model_class.range_key
|
49
|
-
|
50
|
-
|
51
|
-
options[:range_key] = value_dumped
|
57
|
+
range_key_dumped = cast_and_dump(@model_class.range_key, @sort_key)
|
58
|
+
options[:range_key] = range_key_dumped
|
52
59
|
end
|
53
60
|
|
54
61
|
options[:conditions] = @conditions
|
@@ -58,6 +65,12 @@ module Dynamoid
|
|
58
65
|
def undump_attributes(raw_attributes)
|
59
66
|
Undumping.undump_attributes(raw_attributes, @model_class.attributes)
|
60
67
|
end
|
68
|
+
|
69
|
+
def cast_and_dump(name, value)
|
70
|
+
options = @model_class.attributes[name]
|
71
|
+
value_casted = TypeCasting.cast_field(value, options)
|
72
|
+
Dumping.dump_field(value_casted, options)
|
73
|
+
end
|
61
74
|
end
|
62
75
|
end
|
63
76
|
end
|
data/lib/dynamoid/persistence.rb
CHANGED
@@ -185,6 +185,9 @@ module Dynamoid
|
|
185
185
|
#
|
186
186
|
# Validates model and runs callbacks.
|
187
187
|
#
|
188
|
+
# Raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is required
|
189
|
+
# but not specified or has value +nil+.
|
190
|
+
#
|
188
191
|
# @param attrs [Hash|Array<Hash>] Attributes of a model
|
189
192
|
# @param block [Proc] Block to process a document after initialization
|
190
193
|
# @return [Dynamoid::Document] The created document
|
@@ -220,6 +223,9 @@ module Dynamoid
|
|
220
223
|
#
|
221
224
|
# Validates model and runs callbacks.
|
222
225
|
#
|
226
|
+
# Raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is required
|
227
|
+
# but not specified or has value +nil+.
|
228
|
+
#
|
223
229
|
# @param attrs [Hash|Array<Hash>] Attributes with which to create the object.
|
224
230
|
# @param block [Proc] Block to process a document after initialization
|
225
231
|
# @return [Dynamoid::Document] The created document
|
@@ -311,10 +317,16 @@ module Dynamoid
|
|
311
317
|
# Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
|
312
318
|
# attributes is not on the model
|
313
319
|
#
|
320
|
+
# Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
|
321
|
+
# +nil+ and +Dynamoid::Errors::MissingRangeKey+ if a sort key is required
|
322
|
+
# but has value +nil+.
|
323
|
+
#
|
314
324
|
# @param hash_key_value [Scalar value] hash key
|
315
325
|
# @param range_key_value [Scalar value] range key (optional)
|
316
326
|
# @param attrs [Hash]
|
317
327
|
# @param conditions [Hash] (optional)
|
328
|
+
# @option conditions [Hash] :if conditions on attribute values
|
329
|
+
# @option conditions [Hash] :unless_exists conditions on attributes presence
|
318
330
|
# @return [Dynamoid::Document|nil] Updated document
|
319
331
|
def update_fields(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
|
320
332
|
optional_params = [range_key_value, attrs, conditions].compact
|
@@ -369,10 +381,16 @@ module Dynamoid
|
|
369
381
|
# Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
|
370
382
|
# attributes is not declared in the model class.
|
371
383
|
#
|
384
|
+
# Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
|
385
|
+
# +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
|
386
|
+
# required but has value +nil+.
|
387
|
+
#
|
372
388
|
# @param hash_key_value [Scalar value] hash key
|
373
389
|
# @param range_key_value [Scalar value] range key (optional)
|
374
390
|
# @param attrs [Hash]
|
375
391
|
# @param conditions [Hash] (optional)
|
392
|
+
# @option conditions [Hash] :if conditions on attribute values
|
393
|
+
# @option conditions [Hash] :unless_exists conditions on attributes presence
|
376
394
|
# @return [Dynamoid::Document|nil] Updated document
|
377
395
|
def upsert(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
|
378
396
|
optional_params = [range_key_value, attrs, conditions].compact
|
@@ -426,6 +444,7 @@ module Dynamoid
|
|
426
444
|
# @option counters [true | Symbol | Array<Symbol>] :touch to update update_at attribute and optionally the specified ones
|
427
445
|
# @return [Model class] self
|
428
446
|
def inc(hash_key_value, range_key_value = nil, counters)
|
447
|
+
# It's similar to Rails' #update_counters.
|
429
448
|
Inc.call(self, hash_key_value, range_key_value, counters)
|
430
449
|
self
|
431
450
|
end
|
@@ -519,7 +538,8 @@ module Dynamoid
|
|
519
538
|
# If a model is new and hash key (+id+ by default) is not assigned yet
|
520
539
|
# it was assigned implicitly with random UUID value.
|
521
540
|
#
|
522
|
-
# If +lock_version+ attribute is declared it will be incremented. If it's
|
541
|
+
# If +lock_version+ attribute is declared it will be incremented. If it's
|
542
|
+
# blank then it will be initialized with 1.
|
523
543
|
#
|
524
544
|
# +save+ method call raises +Dynamoid::Errors::RecordNotUnique+ exception
|
525
545
|
# if primary key (hash key + optional range key) already exists in a
|
@@ -530,6 +550,11 @@ module Dynamoid
|
|
530
550
|
# already changed concurrently and +lock_version+ was consequently
|
531
551
|
# increased.
|
532
552
|
#
|
553
|
+
# Raises +Dynamoid::Errors::MissingHashKey+ if a model is already persisted
|
554
|
+
# and a partition key has value +nil+ and raises
|
555
|
+
# +Dynamoid::Errors::MissingRangeKey+ if a sort key is required but has
|
556
|
+
# value +nil+.
|
557
|
+
#
|
533
558
|
# When a table is not created yet the first +save+ method call will create
|
534
559
|
# a table. It's useful in test environment to avoid explicit table
|
535
560
|
# creation.
|
@@ -553,6 +578,85 @@ module Dynamoid
|
|
553
578
|
end
|
554
579
|
end
|
555
580
|
|
581
|
+
# Create new model or persist changes.
|
582
|
+
#
|
583
|
+
# Run the validation and callbacks. Raises
|
584
|
+
# +Dynamoid::Errors::DocumentNotValid+ is validation fails.
|
585
|
+
#
|
586
|
+
# user = User.create
|
587
|
+
#
|
588
|
+
# user.age = 26
|
589
|
+
# user.save! # => user
|
590
|
+
#
|
591
|
+
# Validation can be skipped with +validate: false+ option:
|
592
|
+
#
|
593
|
+
# user = User.new(age: -1)
|
594
|
+
# user.save!(validate: false) # => user
|
595
|
+
#
|
596
|
+
# +save!+ by default sets timestamps attributes - +created_at+ and
|
597
|
+
# +updated_at+ when creates new model and updates +updated_at+ attribute
|
598
|
+
# when updates already existing one.
|
599
|
+
#
|
600
|
+
# Changing +updated_at+ attribute at updating a model can be skipped with
|
601
|
+
# +touch: false+ option:
|
602
|
+
#
|
603
|
+
# user.save!(touch: false)
|
604
|
+
#
|
605
|
+
# If a model is new and hash key (+id+ by default) is not assigned yet
|
606
|
+
# it was assigned implicitly with random UUID value.
|
607
|
+
#
|
608
|
+
# If +lock_version+ attribute is declared it will be incremented. If it's
|
609
|
+
# blank then it will be initialized with 1.
|
610
|
+
#
|
611
|
+
# +save!+ method call raises +Dynamoid::Errors::RecordNotUnique+ exception
|
612
|
+
# if primary key (hash key + optional range key) already exists in a
|
613
|
+
# table.
|
614
|
+
#
|
615
|
+
# +save!+ method call raises +Dynamoid::Errors::StaleObjectError+ exception
|
616
|
+
# if there is +lock_version+ attribute and the document in a table was
|
617
|
+
# already changed concurrently and +lock_version+ was consequently
|
618
|
+
# increased.
|
619
|
+
#
|
620
|
+
# +save!+ method call raises +Dynamoid::Errors::RecordNotSaved+ exception
|
621
|
+
# if some callback aborted execution.
|
622
|
+
#
|
623
|
+
# Raises +Dynamoid::Errors::MissingHashKey+ if a model is already persisted
|
624
|
+
# and a partition key has value +nil+ and raises
|
625
|
+
# +Dynamoid::Errors::MissingRangeKey+ if a sort key is required but has
|
626
|
+
# value +nil+.
|
627
|
+
#
|
628
|
+
# When a table is not created yet the first +save!+ method call will create
|
629
|
+
# a table. It's useful in test environment to avoid explicit table
|
630
|
+
# creation.
|
631
|
+
#
|
632
|
+
# @param options [Hash] (optional)
|
633
|
+
# @option options [true|false] :validate validate a model or not - +true+ by default (optional)
|
634
|
+
# @option options [true|false] :touch update tiemstamps fields or not - +true+ by default (optional)
|
635
|
+
# @return [true|false] Whether saving successful or not
|
636
|
+
def save!(options = {})
|
637
|
+
# validation is handled in the Validation module
|
638
|
+
|
639
|
+
if Dynamoid.config.create_table_on_save
|
640
|
+
self.class.create_table(sync: true)
|
641
|
+
end
|
642
|
+
|
643
|
+
create_or_update = new_record? ? :create : :update
|
644
|
+
aborted = true
|
645
|
+
|
646
|
+
run_callbacks(:save) do
|
647
|
+
run_callbacks(create_or_update) do
|
648
|
+
aborted = false
|
649
|
+
Save.call(self, touch: options[:touch])
|
650
|
+
end
|
651
|
+
end
|
652
|
+
|
653
|
+
if aborted
|
654
|
+
raise Dynamoid::Errors::RecordNotSaved, self
|
655
|
+
end
|
656
|
+
|
657
|
+
self
|
658
|
+
end
|
659
|
+
|
556
660
|
# Update multiple attributes at once, saving the object once the updates
|
557
661
|
# are complete.
|
558
662
|
#
|
@@ -564,6 +668,10 @@ module Dynamoid
|
|
564
668
|
# Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
|
565
669
|
# attributes is not on the model
|
566
670
|
#
|
671
|
+
# Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
|
672
|
+
# +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
|
673
|
+
# required but has value +nil+.
|
674
|
+
#
|
567
675
|
# @param attributes [Hash] a hash of attributes to update
|
568
676
|
# @return [true|false] Whether updating successful or not
|
569
677
|
# @since 0.2.0
|
@@ -583,6 +691,10 @@ module Dynamoid
|
|
583
691
|
# Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
|
584
692
|
# attributes is not on the model
|
585
693
|
#
|
694
|
+
# Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
|
695
|
+
# +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
|
696
|
+
# required but has value +nil+.
|
697
|
+
#
|
586
698
|
# @param attributes [Hash] a hash of attributes to update
|
587
699
|
def update_attributes!(attributes)
|
588
700
|
attributes.each { |attribute, value| write_attribute(attribute, value) }
|
@@ -607,9 +719,6 @@ module Dynamoid
|
|
607
719
|
# @since 0.2.0
|
608
720
|
def update_attribute(attribute, value)
|
609
721
|
# final implementation is in the Dynamoid::Validation module
|
610
|
-
write_attribute(attribute, value)
|
611
|
-
save
|
612
|
-
self
|
613
722
|
end
|
614
723
|
|
615
724
|
# Update a model.
|
@@ -620,8 +729,7 @@ module Dynamoid
|
|
620
729
|
# attributes. Supports following operations: +add+, +delete+ and +set+.
|
621
730
|
#
|
622
731
|
# Operation +add+ just adds a value for numeric attributes and join
|
623
|
-
# collections if attribute is a
|
624
|
-
# +map+).
|
732
|
+
# collections if attribute is a set.
|
625
733
|
#
|
626
734
|
# user.update! do |t|
|
627
735
|
# t.add(age: 1, followers_count: 5)
|
@@ -646,7 +754,7 @@ module Dynamoid
|
|
646
754
|
# {parameter}[https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LegacyConditionalParameters.AttributeUpdates.html]
|
647
755
|
# of +UpdateItem+ operation.
|
648
756
|
#
|
649
|
-
# It's
|
757
|
+
# It's atomic operations. So adding or deleting elements in a collection
|
650
758
|
# or incrementing or decrementing a numeric field is atomic and does not
|
651
759
|
# interfere with other write requests.
|
652
760
|
#
|
@@ -686,9 +794,18 @@ module Dynamoid
|
|
686
794
|
|
687
795
|
begin
|
688
796
|
table_name = self.class.table_name
|
797
|
+
partition_key_dumped = Dumping.dump_field(hash_key, self.class.attributes[self.class.hash_key])
|
798
|
+
conditions = conditions.dup
|
799
|
+
conditions[:if] ||= {}
|
800
|
+
conditions[:if][self.class.hash_key] = partition_key_dumped
|
801
|
+
if self.class.range_key
|
802
|
+
sort_key_dumped = Dumping.dump_field(range_value, self.class.attributes[self.class.range_key])
|
803
|
+
conditions[:if][self.class.range_key] = sort_key_dumped
|
804
|
+
end
|
805
|
+
|
689
806
|
update_item_options = options.merge(conditions: conditions)
|
690
807
|
|
691
|
-
new_attrs = Dynamoid.adapter.update_item(table_name,
|
808
|
+
new_attrs = Dynamoid.adapter.update_item(table_name, partition_key_dumped, update_item_options) do |t|
|
692
809
|
item_updater = ItemUpdaterWithDumping.new(self.class, t)
|
693
810
|
|
694
811
|
item_updater.add(lock_version: 1) if self.class.attributes[:lock_version]
|
@@ -701,6 +818,9 @@ module Dynamoid
|
|
701
818
|
end
|
702
819
|
load(Undumping.undump_attributes(new_attrs, self.class.attributes))
|
703
820
|
rescue Dynamoid::Errors::ConditionalCheckFailedException
|
821
|
+
# exception may be raised either because of failed user provided conditions
|
822
|
+
# or because of conditions on partition and sort keys. We cannot
|
823
|
+
# distinguish these two cases.
|
704
824
|
raise Dynamoid::Errors::StaleObjectError.new(self, 'update')
|
705
825
|
end
|
706
826
|
end
|
@@ -716,8 +836,7 @@ module Dynamoid
|
|
716
836
|
# attributes. Supports following operations: +add+, +delete+ and +set+.
|
717
837
|
#
|
718
838
|
# Operation +add+ just adds a value for numeric attributes and join
|
719
|
-
# collections if attribute is a
|
720
|
-
# +map+).
|
839
|
+
# collections if attribute is a set.
|
721
840
|
#
|
722
841
|
# user.update do |t|
|
723
842
|
# t.add(age: 1, followers_count: 5)
|
@@ -891,6 +1010,10 @@ module Dynamoid
|
|
891
1010
|
#
|
892
1011
|
# Returns +self+ if deleted successfully and +false+ otherwise.
|
893
1012
|
#
|
1013
|
+
# Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
|
1014
|
+
# +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
|
1015
|
+
# required but has value +nil+.
|
1016
|
+
#
|
894
1017
|
# @return [Dynamoid::Document|false] whether deleted successfully
|
895
1018
|
# @since 0.2.0
|
896
1019
|
def destroy
|
@@ -912,6 +1035,10 @@ module Dynamoid
|
|
912
1035
|
#
|
913
1036
|
# Raises +Dynamoid::Errors::RecordNotDestroyed+ exception if model deleting
|
914
1037
|
# failed.
|
1038
|
+
#
|
1039
|
+
# Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
|
1040
|
+
# +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
|
1041
|
+
# required but has value +nil+.
|
915
1042
|
def destroy!
|
916
1043
|
destroy || (raise Dynamoid::Errors::RecordNotDestroyed, self)
|
917
1044
|
end
|
@@ -924,10 +1051,18 @@ module Dynamoid
|
|
924
1051
|
# Raises +Dynamoid::Errors::StaleObjectError+ exception if cannot delete a
|
925
1052
|
# model.
|
926
1053
|
#
|
1054
|
+
# Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
|
1055
|
+
# +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
|
1056
|
+
# required but has value +nil+.
|
1057
|
+
#
|
927
1058
|
# @return [Dynamoid::Document] self
|
928
1059
|
# @since 0.2.0
|
929
1060
|
def delete
|
1061
|
+
raise Dynamoid::Errors::MissingHashKey if hash_key.nil?
|
1062
|
+
raise Dynamoid::Errors::MissingRangeKey if self.class.range_key? && range_value.nil?
|
1063
|
+
|
930
1064
|
options = range_key ? { range_key: Dumping.dump_field(read_attribute(range_key), self.class.attributes[range_key]) } : {}
|
1065
|
+
partition_key_dumped = Dumping.dump_field(hash_key, self.class.attributes[self.class.hash_key])
|
931
1066
|
|
932
1067
|
# Add an optimistic locking check if the lock_version column exists
|
933
1068
|
if self.class.attributes[:lock_version]
|
@@ -943,7 +1078,7 @@ module Dynamoid
|
|
943
1078
|
|
944
1079
|
@destroyed = true
|
945
1080
|
|
946
|
-
Dynamoid.adapter.delete(self.class.table_name,
|
1081
|
+
Dynamoid.adapter.delete(self.class.table_name, partition_key_dumped, options)
|
947
1082
|
|
948
1083
|
self.class.associations.each_key do |name|
|
949
1084
|
send(name).disassociate_source
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dynamoid
|
4
|
+
class TransactionRead
|
5
|
+
class Find
|
6
|
+
attr_reader :model_class
|
7
|
+
|
8
|
+
def initialize(model_class, *ids, **options)
|
9
|
+
@model_class = model_class
|
10
|
+
@ids = ids
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def on_registration
|
15
|
+
validate_primary_key!
|
16
|
+
end
|
17
|
+
|
18
|
+
def observable_by_user_result
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def action_request
|
23
|
+
if single_key_given?
|
24
|
+
action_request_for_single_key
|
25
|
+
else
|
26
|
+
action_request_for_multiple_keys
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def process_responses(responses)
|
31
|
+
models = responses.map do |response|
|
32
|
+
if response.item
|
33
|
+
@model_class.from_database(response.item)
|
34
|
+
elsif @options[:raise_error] == false
|
35
|
+
nil
|
36
|
+
else
|
37
|
+
message = build_record_not_found_exception_message(responses)
|
38
|
+
raise Dynamoid::Errors::RecordNotFound, message
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
unless single_key_given?
|
43
|
+
models.compact!
|
44
|
+
end
|
45
|
+
|
46
|
+
models.each { |m| m&.run_callbacks :find }
|
47
|
+
models
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def single_key_given?
|
53
|
+
@ids.size == 1 && !@ids[0].is_a?(Array)
|
54
|
+
end
|
55
|
+
|
56
|
+
def validate_primary_key!
|
57
|
+
if single_key_given?
|
58
|
+
partition_key = @ids[0]
|
59
|
+
sort_key = @options[:range_key]
|
60
|
+
|
61
|
+
raise Dynamoid::Errors::MissingHashKey if partition_key.nil?
|
62
|
+
raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key && sort_key.nil?
|
63
|
+
else
|
64
|
+
ids = @ids.flatten(1)
|
65
|
+
|
66
|
+
raise Dynamoid::Errors::MissingHashKey if ids.any? { |pk, _sk| pk.nil? }
|
67
|
+
raise Errors::MissingRangeKey if @model_class.range_key && ids.any? { |_pk, sk| sk.nil? }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def action_request_for_single_key
|
72
|
+
partition_key = @ids[0]
|
73
|
+
|
74
|
+
key = { @model_class.hash_key => cast_and_dump_attribute(@model_class.hash_key, partition_key) }
|
75
|
+
|
76
|
+
if @model_class.range_key
|
77
|
+
sort_key = @options[:range_key]
|
78
|
+
key[@model_class.range_key] = cast_and_dump_attribute(@model_class.range_key, sort_key)
|
79
|
+
end
|
80
|
+
|
81
|
+
{
|
82
|
+
get: {
|
83
|
+
key: key,
|
84
|
+
table_name: @model_class.table_name
|
85
|
+
}
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
def action_request_for_multiple_keys
|
90
|
+
@ids.flatten(1).map do |id|
|
91
|
+
if @model_class.range_key
|
92
|
+
# expect [hash-key, range-key] pair
|
93
|
+
pk, sk = id
|
94
|
+
pk_dumped = cast_and_dump_attribute(@model_class.hash_key, pk)
|
95
|
+
sk_dumped = cast_and_dump_attribute(@model_class.range_key, sk)
|
96
|
+
|
97
|
+
key = { @model_class.hash_key => pk_dumped, @model_class.range_key => sk_dumped }
|
98
|
+
else
|
99
|
+
pk_dumped = cast_and_dump_attribute(@model_class.hash_key, id)
|
100
|
+
|
101
|
+
key = { @model_class.hash_key => pk_dumped }
|
102
|
+
end
|
103
|
+
|
104
|
+
{
|
105
|
+
get: {
|
106
|
+
key: key,
|
107
|
+
table_name: @model_class.table_name
|
108
|
+
}
|
109
|
+
}
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def cast_and_dump_attribute(name, value)
|
114
|
+
attribute_options = @model_class.attributes[name]
|
115
|
+
casted_value = TypeCasting.cast_field(value, attribute_options)
|
116
|
+
Dumping.dump_field(casted_value, attribute_options)
|
117
|
+
end
|
118
|
+
|
119
|
+
def build_record_not_found_exception_message(responses)
|
120
|
+
items = responses.map(&:item)
|
121
|
+
ids = @ids.flatten(1)
|
122
|
+
|
123
|
+
if single_key_given?
|
124
|
+
id = ids[0]
|
125
|
+
primary_key = @model_class.range_key ? "(#{id.inspect},#{@options[:range_key].inspect})" : id.inspect
|
126
|
+
message = "Couldn't find #{@model_class.name} with primary key #{primary_key}"
|
127
|
+
else
|
128
|
+
ids_list = @model_class.range_key ? ids.map { |pk, sk| "(#{pk.inspect},#{sk.inspect})" } : ids.map(&:inspect)
|
129
|
+
message = "Couldn't find all #{@model_class.name.pluralize} with primary keys [#{ids_list.join(', ')}] "
|
130
|
+
message += "(found #{items.compact.size} results, but was looking for #{items.size})"
|
131
|
+
end
|
132
|
+
|
133
|
+
message
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|