dynamoid 3.10.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 +37 -1
- data/README.md +268 -8
- data/dynamoid.gemspec +4 -4
- data/lib/dynamoid/adapter.rb +1 -1
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/filter_expression_convertor.rb +53 -18
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +5 -4
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/projection_expression_convertor.rb +9 -7
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +1 -1
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +1 -1
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/transact.rb +31 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +17 -5
- data/lib/dynamoid/components.rb +1 -0
- data/lib/dynamoid/config.rb +3 -0
- data/lib/dynamoid/criteria/chain.rb +74 -21
- data/lib/dynamoid/criteria/where_conditions.rb +13 -6
- data/lib/dynamoid/dirty.rb +97 -11
- data/lib/dynamoid/dumping.rb +39 -17
- data/lib/dynamoid/errors.rb +30 -3
- data/lib/dynamoid/fields.rb +13 -3
- data/lib/dynamoid/finders.rb +44 -23
- data/lib/dynamoid/loadable.rb +1 -0
- data/lib/dynamoid/persistence/inc.rb +35 -19
- data/lib/dynamoid/persistence/item_updater_with_casting_and_dumping.rb +36 -0
- data/lib/dynamoid/persistence/item_updater_with_dumping.rb +33 -0
- data/lib/dynamoid/persistence/save.rb +29 -14
- data/lib/dynamoid/persistence/update_fields.rb +23 -8
- data/lib/dynamoid/persistence/update_validations.rb +3 -3
- data/lib/dynamoid/persistence/upsert.rb +22 -8
- data/lib/dynamoid/persistence.rb +184 -28
- data/lib/dynamoid/transaction_read/find.rb +137 -0
- data/lib/dynamoid/transaction_read.rb +146 -0
- data/lib/dynamoid/transaction_write/base.rb +47 -0
- data/lib/dynamoid/transaction_write/create.rb +49 -0
- data/lib/dynamoid/transaction_write/delete_with_instance.rb +65 -0
- data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +64 -0
- data/lib/dynamoid/transaction_write/destroy.rb +84 -0
- data/lib/dynamoid/transaction_write/item_updater.rb +55 -0
- data/lib/dynamoid/transaction_write/save.rb +169 -0
- data/lib/dynamoid/transaction_write/update_attributes.rb +46 -0
- data/lib/dynamoid/transaction_write/update_fields.rb +239 -0
- data/lib/dynamoid/transaction_write/upsert.rb +106 -0
- data/lib/dynamoid/transaction_write.rb +673 -0
- data/lib/dynamoid/type_casting.rb +3 -1
- data/lib/dynamoid/undumping.rb +13 -2
- data/lib/dynamoid/validations.rb +8 -5
- data/lib/dynamoid/version.rb +1 -1
- data/lib/dynamoid.rb +8 -0
- metadata +21 -5
data/lib/dynamoid/persistence.rb
CHANGED
@@ -10,6 +10,7 @@ require 'dynamoid/persistence/upsert'
|
|
10
10
|
require 'dynamoid/persistence/save'
|
11
11
|
require 'dynamoid/persistence/inc'
|
12
12
|
require 'dynamoid/persistence/update_validations'
|
13
|
+
require 'dynamoid/persistence/item_updater_with_dumping'
|
13
14
|
|
14
15
|
# encoding: utf-8
|
15
16
|
module Dynamoid
|
@@ -18,8 +19,9 @@ module Dynamoid
|
|
18
19
|
module Persistence
|
19
20
|
extend ActiveSupport::Concern
|
20
21
|
|
21
|
-
attr_accessor :new_record
|
22
|
+
attr_accessor :new_record, :destroyed
|
22
23
|
alias new_record? new_record
|
24
|
+
alias destroyed? destroyed
|
23
25
|
|
24
26
|
# @private
|
25
27
|
UNIX_EPOCH_DATE = Date.new(1970, 1, 1).freeze
|
@@ -166,7 +168,7 @@ module Dynamoid
|
|
166
168
|
|
167
169
|
# Create a model.
|
168
170
|
#
|
169
|
-
# Initializes a new model and immediately saves it
|
171
|
+
# Initializes a new model and immediately saves it into DynamoDB.
|
170
172
|
#
|
171
173
|
# User.create(first_name: 'Mark', last_name: 'Tyler')
|
172
174
|
#
|
@@ -174,7 +176,8 @@ module Dynamoid
|
|
174
176
|
#
|
175
177
|
# User.create([{ first_name: 'Alice' }, { first_name: 'Bob' }])
|
176
178
|
#
|
177
|
-
#
|
179
|
+
# Instantiates a model and pass it into an optional block to set other
|
180
|
+
# attributes.
|
178
181
|
#
|
179
182
|
# User.create(first_name: 'Mark') do |u|
|
180
183
|
# u.age = 21
|
@@ -182,7 +185,10 @@ module Dynamoid
|
|
182
185
|
#
|
183
186
|
# Validates model and runs callbacks.
|
184
187
|
#
|
185
|
-
#
|
188
|
+
# Raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is required
|
189
|
+
# but not specified or has value +nil+.
|
190
|
+
#
|
191
|
+
# @param attrs [Hash|Array<Hash>] Attributes of a model
|
186
192
|
# @param block [Proc] Block to process a document after initialization
|
187
193
|
# @return [Dynamoid::Document] The created document
|
188
194
|
# @since 0.2.0
|
@@ -196,12 +202,31 @@ module Dynamoid
|
|
196
202
|
|
197
203
|
# Create a model.
|
198
204
|
#
|
199
|
-
# Initializes a new object and immediately saves it
|
205
|
+
# Initializes a new object and immediately saves it into DynamoDB.
|
206
|
+
#
|
207
|
+
# User.create!(first_name: 'Mark', last_name: 'Tyler')
|
208
|
+
#
|
200
209
|
# Raises an exception +Dynamoid::Errors::DocumentNotValid+ if validation
|
201
|
-
# failed.
|
210
|
+
# failed.
|
211
|
+
#
|
212
|
+
# Accepts both Hash and Array of Hashes and can create several
|
202
213
|
# models.
|
203
214
|
#
|
204
|
-
#
|
215
|
+
# User.create!([{ first_name: 'Alice' }, { first_name: 'Bob' }])
|
216
|
+
#
|
217
|
+
# Instantiates a model and pass it into an optional block to set other
|
218
|
+
# attributes.
|
219
|
+
#
|
220
|
+
# User.create!(first_name: 'Mark') do |u|
|
221
|
+
# u.age = 21
|
222
|
+
# end
|
223
|
+
#
|
224
|
+
# Validates model and runs callbacks.
|
225
|
+
#
|
226
|
+
# Raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is required
|
227
|
+
# but not specified or has value +nil+.
|
228
|
+
#
|
229
|
+
# @param attrs [Hash|Array<Hash>] Attributes with which to create the object.
|
205
230
|
# @param block [Proc] Block to process a document after initialization
|
206
231
|
# @return [Dynamoid::Document] The created document
|
207
232
|
# @since 0.2.0
|
@@ -292,10 +317,16 @@ module Dynamoid
|
|
292
317
|
# Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
|
293
318
|
# attributes is not on the model
|
294
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
|
+
#
|
295
324
|
# @param hash_key_value [Scalar value] hash key
|
296
325
|
# @param range_key_value [Scalar value] range key (optional)
|
297
326
|
# @param attrs [Hash]
|
298
327
|
# @param conditions [Hash] (optional)
|
328
|
+
# @option conditions [Hash] :if conditions on attribute values
|
329
|
+
# @option conditions [Hash] :unless_exists conditions on attributes presence
|
299
330
|
# @return [Dynamoid::Document|nil] Updated document
|
300
331
|
def update_fields(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
|
301
332
|
optional_params = [range_key_value, attrs, conditions].compact
|
@@ -348,12 +379,18 @@ module Dynamoid
|
|
348
379
|
# an updated document back with one HTTP request.
|
349
380
|
#
|
350
381
|
# Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
|
351
|
-
# attributes is not
|
382
|
+
# attributes is not declared in the model class.
|
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+.
|
352
387
|
#
|
353
388
|
# @param hash_key_value [Scalar value] hash key
|
354
389
|
# @param range_key_value [Scalar value] range key (optional)
|
355
390
|
# @param attrs [Hash]
|
356
391
|
# @param conditions [Hash] (optional)
|
392
|
+
# @option conditions [Hash] :if conditions on attribute values
|
393
|
+
# @option conditions [Hash] :unless_exists conditions on attributes presence
|
357
394
|
# @return [Dynamoid::Document|nil] Updated document
|
358
395
|
def upsert(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
|
359
396
|
optional_params = [range_key_value, attrs, conditions].compact
|
@@ -404,9 +441,10 @@ module Dynamoid
|
|
404
441
|
# @param hash_key_value [Scalar value] hash key
|
405
442
|
# @param range_key_value [Scalar value] range key (optional)
|
406
443
|
# @param counters [Hash] value to increase by
|
407
|
-
# @option counters [true | Symbol | Array
|
444
|
+
# @option counters [true | Symbol | Array<Symbol>] :touch to update update_at attribute and optionally the specified ones
|
408
445
|
# @return [Model class] self
|
409
446
|
def inc(hash_key_value, range_key_value = nil, counters)
|
447
|
+
# It's similar to Rails' #update_counters.
|
410
448
|
Inc.call(self, hash_key_value, range_key_value, counters)
|
411
449
|
self
|
412
450
|
end
|
@@ -490,7 +528,7 @@ module Dynamoid
|
|
490
528
|
#
|
491
529
|
# +save+ by default sets timestamps attributes - +created_at+ and
|
492
530
|
# +updated_at+ when creates new model and updates +updated_at+ attribute
|
493
|
-
# when
|
531
|
+
# when updates already existing one.
|
494
532
|
#
|
495
533
|
# Changing +updated_at+ attribute at updating a model can be skipped with
|
496
534
|
# +touch: false+ option:
|
@@ -500,7 +538,8 @@ module Dynamoid
|
|
500
538
|
# If a model is new and hash key (+id+ by default) is not assigned yet
|
501
539
|
# it was assigned implicitly with random UUID value.
|
502
540
|
#
|
503
|
-
# 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.
|
504
543
|
#
|
505
544
|
# +save+ method call raises +Dynamoid::Errors::RecordNotUnique+ exception
|
506
545
|
# if primary key (hash key + optional range key) already exists in a
|
@@ -511,6 +550,11 @@ module Dynamoid
|
|
511
550
|
# already changed concurrently and +lock_version+ was consequently
|
512
551
|
# increased.
|
513
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
|
+
#
|
514
558
|
# When a table is not created yet the first +save+ method call will create
|
515
559
|
# a table. It's useful in test environment to avoid explicit table
|
516
560
|
# creation.
|
@@ -534,8 +578,89 @@ module Dynamoid
|
|
534
578
|
end
|
535
579
|
end
|
536
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
|
+
|
537
660
|
# Update multiple attributes at once, saving the object once the updates
|
538
|
-
# are complete.
|
661
|
+
# are complete.
|
662
|
+
#
|
663
|
+
# Returns +true+ if saving is successful and +false+
|
539
664
|
# otherwise.
|
540
665
|
#
|
541
666
|
# user.update_attributes(age: 27, last_name: 'Tylor')
|
@@ -543,6 +668,10 @@ module Dynamoid
|
|
543
668
|
# Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
|
544
669
|
# attributes is not on the model
|
545
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
|
+
#
|
546
675
|
# @param attributes [Hash] a hash of attributes to update
|
547
676
|
# @return [true|false] Whether updating successful or not
|
548
677
|
# @since 0.2.0
|
@@ -562,6 +691,10 @@ module Dynamoid
|
|
562
691
|
# Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
|
563
692
|
# attributes is not on the model
|
564
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
|
+
#
|
565
698
|
# @param attributes [Hash] a hash of attributes to update
|
566
699
|
def update_attributes!(attributes)
|
567
700
|
attributes.each { |attribute, value| write_attribute(attribute, value) }
|
@@ -586,9 +719,6 @@ module Dynamoid
|
|
586
719
|
# @since 0.2.0
|
587
720
|
def update_attribute(attribute, value)
|
588
721
|
# final implementation is in the Dynamoid::Validation module
|
589
|
-
write_attribute(attribute, value)
|
590
|
-
save
|
591
|
-
self
|
592
722
|
end
|
593
723
|
|
594
724
|
# Update a model.
|
@@ -599,8 +729,7 @@ module Dynamoid
|
|
599
729
|
# attributes. Supports following operations: +add+, +delete+ and +set+.
|
600
730
|
#
|
601
731
|
# Operation +add+ just adds a value for numeric attributes and join
|
602
|
-
# collections if attribute is a
|
603
|
-
# +map+).
|
732
|
+
# collections if attribute is a set.
|
604
733
|
#
|
605
734
|
# user.update! do |t|
|
606
735
|
# t.add(age: 1, followers_count: 5)
|
@@ -625,7 +754,7 @@ module Dynamoid
|
|
625
754
|
# {parameter}[https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LegacyConditionalParameters.AttributeUpdates.html]
|
626
755
|
# of +UpdateItem+ operation.
|
627
756
|
#
|
628
|
-
# It's
|
757
|
+
# It's atomic operations. So adding or deleting elements in a collection
|
629
758
|
# or incrementing or decrementing a numeric field is atomic and does not
|
630
759
|
# interfere with other write requests.
|
631
760
|
#
|
@@ -665,21 +794,33 @@ module Dynamoid
|
|
665
794
|
|
666
795
|
begin
|
667
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
|
+
|
668
806
|
update_item_options = options.merge(conditions: conditions)
|
669
807
|
|
670
|
-
new_attrs = Dynamoid.adapter.update_item(table_name,
|
671
|
-
|
808
|
+
new_attrs = Dynamoid.adapter.update_item(table_name, partition_key_dumped, update_item_options) do |t|
|
809
|
+
item_updater = ItemUpdaterWithDumping.new(self.class, t)
|
810
|
+
|
811
|
+
item_updater.add(lock_version: 1) if self.class.attributes[:lock_version]
|
672
812
|
|
673
813
|
if self.class.timestamps_enabled?
|
674
|
-
|
675
|
-
time_now_dumped = Dumping.dump_field(time_now, self.class.attributes[:updated_at])
|
676
|
-
t.set(updated_at: time_now_dumped)
|
814
|
+
item_updater.set(updated_at: DateTime.now.in_time_zone(Time.zone))
|
677
815
|
end
|
678
816
|
|
679
817
|
yield t
|
680
818
|
end
|
681
819
|
load(Undumping.undump_attributes(new_attrs, self.class.attributes))
|
682
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.
|
683
824
|
raise Dynamoid::Errors::StaleObjectError.new(self, 'update')
|
684
825
|
end
|
685
826
|
end
|
@@ -695,8 +836,7 @@ module Dynamoid
|
|
695
836
|
# attributes. Supports following operations: +add+, +delete+ and +set+.
|
696
837
|
#
|
697
838
|
# Operation +add+ just adds a value for numeric attributes and join
|
698
|
-
# collections if attribute is a
|
699
|
-
# +map+).
|
839
|
+
# collections if attribute is a set.
|
700
840
|
#
|
701
841
|
# user.update do |t|
|
702
842
|
# t.add(age: 1, followers_count: 5)
|
@@ -804,7 +944,7 @@ module Dynamoid
|
|
804
944
|
#
|
805
945
|
# @param attribute [Symbol] attribute name
|
806
946
|
# @param by [Numeric] value to add (optional)
|
807
|
-
# @param touch [true | Symbol | Array
|
947
|
+
# @param touch [true | Symbol | Array<Symbol>] to update update_at attribute and optionally the specified ones
|
808
948
|
# @return [Dynamoid::Document] self
|
809
949
|
def increment!(attribute, by = 1, touch: nil)
|
810
950
|
increment(attribute, by)
|
@@ -855,7 +995,7 @@ module Dynamoid
|
|
855
995
|
#
|
856
996
|
# @param attribute [Symbol] attribute name
|
857
997
|
# @param by [Numeric] value to subtract (optional)
|
858
|
-
# @param touch [true | Symbol | Array
|
998
|
+
# @param touch [true | Symbol | Array<Symbol>] to update update_at attribute and optionally the specified ones
|
859
999
|
# @return [Dynamoid::Document] self
|
860
1000
|
def decrement!(attribute, by = 1, touch: nil)
|
861
1001
|
increment!(attribute, -by, touch: touch)
|
@@ -870,6 +1010,10 @@ module Dynamoid
|
|
870
1010
|
#
|
871
1011
|
# Returns +self+ if deleted successfully and +false+ otherwise.
|
872
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
|
+
#
|
873
1017
|
# @return [Dynamoid::Document|false] whether deleted successfully
|
874
1018
|
# @since 0.2.0
|
875
1019
|
def destroy
|
@@ -891,6 +1035,10 @@ module Dynamoid
|
|
891
1035
|
#
|
892
1036
|
# Raises +Dynamoid::Errors::RecordNotDestroyed+ exception if model deleting
|
893
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+.
|
894
1042
|
def destroy!
|
895
1043
|
destroy || (raise Dynamoid::Errors::RecordNotDestroyed, self)
|
896
1044
|
end
|
@@ -903,10 +1051,18 @@ module Dynamoid
|
|
903
1051
|
# Raises +Dynamoid::Errors::StaleObjectError+ exception if cannot delete a
|
904
1052
|
# model.
|
905
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
|
+
#
|
906
1058
|
# @return [Dynamoid::Document] self
|
907
1059
|
# @since 0.2.0
|
908
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
|
+
|
909
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])
|
910
1066
|
|
911
1067
|
# Add an optimistic locking check if the lock_version column exists
|
912
1068
|
if self.class.attributes[:lock_version]
|
@@ -922,7 +1078,7 @@ module Dynamoid
|
|
922
1078
|
|
923
1079
|
@destroyed = true
|
924
1080
|
|
925
|
-
Dynamoid.adapter.delete(self.class.table_name,
|
1081
|
+
Dynamoid.adapter.delete(self.class.table_name, partition_key_dumped, options)
|
926
1082
|
|
927
1083
|
self.class.associations.each_key do |name|
|
928
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
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dynamoid/transaction_read/find'
|
4
|
+
|
5
|
+
module Dynamoid
|
6
|
+
# The class +TransactionRead+ provides means to perform multiple reading
|
7
|
+
# operations in transaction, that is atomically, so that either all of them
|
8
|
+
# succeed, or all of them fail.
|
9
|
+
#
|
10
|
+
# The reading methods are supposed to be as close as possible to their
|
11
|
+
# non-transactional counterparts:
|
12
|
+
#
|
13
|
+
# user_id = params[:user_id]
|
14
|
+
# payment = params[:payment_id]
|
15
|
+
#
|
16
|
+
# models = Dynamoid::TransactionRead.execute do |t|
|
17
|
+
# t.find User, user_id
|
18
|
+
# t.find Payment, payment_id
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# The only difference is that the methods are called on a transaction
|
22
|
+
# instance and a model or a model class should be specified. So +User.find+
|
23
|
+
# becomes +t.find(user_id)+ and +Payment.find(payment_id)+ becomes +t.find
|
24
|
+
# Payment, payment_id+.
|
25
|
+
#
|
26
|
+
# A transaction can be used without a block. This way a transaction instance
|
27
|
+
# should be instantiated and committed manually with +#commit+ method:
|
28
|
+
#
|
29
|
+
# t = Dynamoid::TransactionRead.new
|
30
|
+
#
|
31
|
+
# t.find user_id
|
32
|
+
# t.find payment_id
|
33
|
+
#
|
34
|
+
# models = t.commit
|
35
|
+
#
|
36
|
+
#
|
37
|
+
# ### DynamoDB's transactions
|
38
|
+
#
|
39
|
+
# The main difference between DynamoDB transactions and a common interface is
|
40
|
+
# that DynamoDB's transactions are executed in batch. So in Dynamoid no
|
41
|
+
# data actually loaded when some transactional method (e.g+ `#find+) is
|
42
|
+
# called. All the changes are loaded at the end.
|
43
|
+
#
|
44
|
+
# A +TransactGetItems+ DynamoDB operation is used (see
|
45
|
+
# [documentation](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactGetItems.html)
|
46
|
+
# for details).
|
47
|
+
class TransactionRead
|
48
|
+
def self.execute
|
49
|
+
transaction = new
|
50
|
+
|
51
|
+
begin
|
52
|
+
yield transaction
|
53
|
+
transaction.commit
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def initialize
|
58
|
+
@actions = []
|
59
|
+
end
|
60
|
+
|
61
|
+
# Load all the models.
|
62
|
+
#
|
63
|
+
# transaction = Dynamoid::TransactionRead.new
|
64
|
+
# # ...
|
65
|
+
# transaction.commit
|
66
|
+
def commit
|
67
|
+
return [] if @actions.empty?
|
68
|
+
|
69
|
+
# some actions may produce multiple requests
|
70
|
+
action_request_groups = @actions.map(&:action_request).map do |action_request|
|
71
|
+
action_request.is_a?(Array) ? action_request : [action_request]
|
72
|
+
end
|
73
|
+
action_requests = action_request_groups.flatten(1)
|
74
|
+
|
75
|
+
return [] if action_requests.empty?
|
76
|
+
|
77
|
+
response = Dynamoid.adapter.transact_read_items(action_requests)
|
78
|
+
|
79
|
+
responses = response.responses.dup
|
80
|
+
@actions.zip(action_request_groups).map do |action, action_requests|
|
81
|
+
action_responses = responses.shift(action_requests.size)
|
82
|
+
action.process_responses(action_responses)
|
83
|
+
end.flatten
|
84
|
+
end
|
85
|
+
|
86
|
+
# Find one or many objects, specified by one id or an array of ids.
|
87
|
+
#
|
88
|
+
# By default it raises +RecordNotFound+ exception if at least one model
|
89
|
+
# isn't found. This behavior can be changed with +raise_error+ option. If
|
90
|
+
# specified +raise_error: false+ option then +find+ will not raise the
|
91
|
+
# exception.
|
92
|
+
#
|
93
|
+
# When a document schema includes range key it should always be specified
|
94
|
+
# in +find+ method call. In case it's missing +MissingRangeKey+ exception
|
95
|
+
# will be raised.
|
96
|
+
#
|
97
|
+
# Please note that there are the following differences between
|
98
|
+
# transactional and non-transactional +find+:
|
99
|
+
# - transactional +find+ preserves order of models in result when given multiple ids
|
100
|
+
# - transactional +find+ doesn't return results immediately, a single
|
101
|
+
# collection with results of all the +find+ calls is returned instead
|
102
|
+
# - +:consistent_read+ option isn't supported
|
103
|
+
# - transactional +find+ is subject to limitations of the
|
104
|
+
# [TransactGetItems](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactGetItems.html)
|
105
|
+
# DynamoDB operation, e.g. the whole read transaction can load only up to 100 models.
|
106
|
+
#
|
107
|
+
# @param [Object|Array] ids a single primary key or an array of primary keys
|
108
|
+
# @param [Hash] options optional parameters of the operation
|
109
|
+
# @option options [Object] :range_key sort key of a model; required when a single partition key is given and a sort key is declared for a model
|
110
|
+
# @option options [true|false] :raise_error whether to raise a +RecordNotFound+ exception; specify explicitly +raise_error: false+ to suppress the exception; default is +true+
|
111
|
+
# @return [nil]
|
112
|
+
#
|
113
|
+
# @example Find by partition key
|
114
|
+
# Dynamoid::TransactionRead.execute do |t|
|
115
|
+
# t.find Document, 101
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# @example Find by partition key and sort key
|
119
|
+
# Dynamoid::TransactionRead.execute do |t|
|
120
|
+
# t.find Document, 101, range_key: 'archived'
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
# @example Find several documents by partition key
|
124
|
+
# Dynamoid::TransactionRead.execute do |t|
|
125
|
+
# t.find Document, 101, 102, 103
|
126
|
+
# t.find Document, [101, 102, 103]
|
127
|
+
# end
|
128
|
+
#
|
129
|
+
# @example Find several documents by partition key and sort key
|
130
|
+
# Dynamoid::TransactionRead.execute do |t|
|
131
|
+
# t.find Document, [[101, 'archived'], [102, 'new'], [103, 'deleted']]
|
132
|
+
# end
|
133
|
+
def find(model_class, *ids, **options)
|
134
|
+
action = Dynamoid::TransactionRead::Find.new(model_class, *ids, **options)
|
135
|
+
register_action action
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def register_action(action)
|
141
|
+
@actions << action
|
142
|
+
action.on_registration
|
143
|
+
action.observable_by_user_result
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|