Class: Dynamoid::TransactionWrite
- Inherits:
-
Object
- Object
- Dynamoid::TransactionWrite
- Defined in:
- lib/dynamoid/transaction_write.rb,
lib/dynamoid/transaction_write/base.rb,
lib/dynamoid/transaction_write/save.rb,
lib/dynamoid/transaction_write/create.rb,
lib/dynamoid/transaction_write/upsert.rb,
lib/dynamoid/transaction_write/destroy.rb,
lib/dynamoid/transaction_write/item_updater.rb,
lib/dynamoid/transaction_write/update_fields.rb,
lib/dynamoid/transaction_write/update_attributes.rb,
lib/dynamoid/transaction_write/delete_with_instance.rb,
lib/dynamoid/transaction_write/delete_with_primary_key.rb
Overview
The class TransactionWrite provides means to perform multiple modifying operations in transaction, that is atomically, so that either all of them succeed, or all of them fail.
The persisting methods are supposed to be as close as possible to their non-transactional counterparts like .create, #save and #delete:
user = User.new()
payment = Payment.find(1)
Dynamoid::TransactionWrite.execute do |t|
t.save! user
t.create! Account, name: 'A'
t.delete payment
end
The only difference is that the methods are called on a transaction instance and a model or a model class should be specified.
So user.save! becomes t.save!(user), Account.create!(name: ‘A’) becomes t.create!(Account, name: ‘A’), and payment.delete becomes t.delete(payment).
A transaction can be used without a block. This way a transaction instance should be instantiated and committed manually with #commit method:
t = Dynamoid::TransactionWrite.new
t.save! user
t.create! Account, name: 'A'
t.delete payment
t.commit
Some persisting methods are intentionally not available in a transaction, e.g. .update and .update! that simply call .find and #update_attributes methods. These methods perform multiple operations so cannot be implemented in a transactional atomic way.
### DynamoDB’s transactions
The main difference between DynamoDB transactions and a common interface is that DynamoDB’s transactions are executed in batch. So in Dynamoid no changes are actually persisted when some transactional method (e.g+ ‘#save+) is called. All the changes are persisted at the end.
A TransactWriteItems DynamoDB operation is used (see [documentation](docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html) for details).
### Callbacks
The transactional methods support before_, after_ and around_ callbacks to the extend the non-transactional methods support them.
There is important difference - a transactional method runs callbacks immediately (even after_ ones) when it is called before changes are actually persisted. So code in after_ callbacks does not see observes them in DynamoDB and so for.
When a callback aborts persisting of a model or a model is invalid then transaction is not aborted and may commit successfully.
### Transaction rollback
A transaction is rolled back on DynamoDB’s side automatically when:
-
an ongoing operation is in the process of updating the same item.
-
there is insufficient provisioned capacity for the transaction to be completed.
-
an item size becomes too large (bigger than 400 KB), a local secondary index (LSI) becomes too large, or a similar validation error occurs because of changes made by the transaction.
-
the aggregate size of the items in the transaction exceeds 4 MB.
-
there is a user error, such as an invalid data format.
A transaction can be interrupted simply by an exception raised within a block. As far as no changes are actually persisted before the #commit method call - there is nothing to undo on the DynamoDB’s site.
Raising Dynamoid::Errors::Rollback exception leads to interrupting a transation and it isn’t propogated:
Dynamoid::TransactionWrite.execute do |t|
t.save! user
t.create! Account, name: 'A'
if user.is_admin?
raise Dynamoid::Errors::Rollback
end
end
When a transaction is successfully committed or rolled backed - corresponding #after_commit or #after_rollback callbacks are run for each involved model.
Defined Under Namespace
Classes: Base, Create, DeleteWithInstance, DeleteWithPrimaryKey, Destroy, ItemUpdater, Save, UpdateAttributes, UpdateFields, Upsert
Class Method Summary collapse
Instance Method Summary collapse
-
#commit ⇒ Object
Persist all the changes.
-
#create(model_class, attributes = {}, &block) ⇒ Dynamoid::Document
Create a model.
-
#create!(model_class, attributes = {}, &block) ⇒ Dynamoid::Document
Create a model.
-
#delete(model_or_model_class, hash_key = nil, range_key = nil) ⇒ Dynamoid::Document
Delete a model.
-
#destroy(model) ⇒ Dynamoid::Document
Delete a model.
-
#destroy!(model) ⇒ Dynamoid::Document|false
Delete a model.
-
#initialize ⇒ TransactionWrite
constructor
A new instance of TransactionWrite.
- #rollback ⇒ Object
-
#save(model, **options) ⇒ true|false
Create new model or persist changes in already existing one.
-
#save!(model, **options) ⇒ true|false
Create new model or persist changes in already existing one.
-
#update_attributes(model, attributes) ⇒ true|false
Update multiple attributes at once.
-
#update_attributes!(model, attributes) ⇒ Object
Update multiple attributes at once.
-
#update_fields(model_class, hash_key, range_key = nil, attributes = nil, &block) ⇒ nil
Update document.
-
#upsert(model_class, hash_key, range_key = nil, attributes) ⇒ nil
Update an existing document or create a new one.
Constructor Details
#initialize ⇒ TransactionWrite
Returns a new instance of TransactionWrite.
125 126 127 |
# File 'lib/dynamoid/transaction_write.rb', line 125 def initialize @actions = [] end |
Class Method Details
.execute ⇒ Object
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/dynamoid/transaction_write.rb', line 109 def self.execute transaction = new begin yield transaction rescue StandardError => e transaction.rollback unless e.is_a?(Dynamoid::Errors::Rollback) raise e end else transaction.commit end end |
Instance Method Details
#commit ⇒ Object
Persist all the changes.
transaction = Dynamoid::TransactionWrite.new
# ...
transaction.commit
134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/dynamoid/transaction_write.rb', line 134 def commit actions_to_commit = @actions.reject(&:aborted?).reject(&:skipped?) return if actions_to_commit.empty? action_requests = actions_to_commit.map(&:action_request) Dynamoid.adapter.transact_write_items(action_requests) actions_to_commit.each(&:on_commit) nil rescue Aws::Errors::ServiceError run_on_rollback_callbacks raise end |
#create(model_class, attributes = {}, &block) ⇒ Dynamoid::Document
Create a model.
Dynamoid::TransactionWrite.execute do |t|
t.create(User, name: 'A')
end
Accepts both Hash and Array of Hashes and can create several models.
Dynamoid::TransactionWrite.execute do |t|
t.create(User, [{name: 'A'}, {name: 'B'}, {name: 'C'}])
end
Instantiates a model and pass it into an optional block to set other attributes.
Dynamoid::TransactionWrite.execute do |t|
t.create(User, name: 'A') do |user|
user.initialize_roles
end
end
Validates model and runs callbacks.
Raises Dynamoid::Errors::MissingRangeKey if a sort key is required but not specified or has value nil.
There are the following differences between transactional and non-transactional #create:
-
transactional
#createdoesn’t supportlock_versionattribute so it will not be incremented and will not be checked to detect concurrent modification of a model andDynamoid::Errors::StaleObjectErrorexception will not be raised -
transactional
#createdoesn’t raiseDynamoid::Errors::RecordNotUniqueat saving new model when primary key is already used. A genericAws::DynamoDB::Errors::TransactionCanceledExceptionis raised instead. -
a table isn’t created lazily if it doesn’t exist yet
361 362 363 364 365 366 367 368 369 370 371 |
# File 'lib/dynamoid/transaction_write.rb', line 361 def create(model_class, attributes = {}, &block) if attributes.is_a? Array attributes.map do |attr| action = Dynamoid::TransactionWrite::Create.new(model_class, attr, raise_error: false, &block) register_action action end else action = Dynamoid::TransactionWrite::Create.new(model_class, attributes, raise_error: false, &block) register_action action end end |
#create!(model_class, attributes = {}, &block) ⇒ Dynamoid::Document
Create a model.
Dynamoid::TransactionWrite.execute do |t|
t.create!(User, name: 'A')
end
Accepts both Hash and Array of Hashes and can create several models.
Dynamoid::TransactionWrite.execute do |t|
t.create!(User, [{name: 'A'}, {name: 'B'}, {name: 'C'}])
end
Instantiates a model and pass it into an optional block to set other attributes.
Dynamoid::TransactionWrite.execute do |t|
t.create!(User, name: 'A') do |user|
user.initialize_roles
end
end
Validates model and runs callbacks.
Raises Dynamoid::Errors::MissingRangeKey if a sort key is required but not specified or has value nil.
There are the following differences between transactional and non-transactional #create:
-
transactional
#create!doesn’t supportlock_versionattribute so it will not be incremented and will not be checked to detect concurrent modification of a model andDynamoid::Errors::StaleObjectErrorexception will not be raised -
transactional
#create!doesn’t raiseDynamoid::Errors::RecordNotUniqueat saving new model when primary key is already used. A genericAws::DynamoDB::Errors::TransactionCanceledExceptionis raised instead. -
a table isn’t created lazily if it doesn’t exist yet
308 309 310 311 312 313 314 315 316 317 318 |
# File 'lib/dynamoid/transaction_write.rb', line 308 def create!(model_class, attributes = {}, &block) if attributes.is_a? Array attributes.map do |attr| action = Dynamoid::TransactionWrite::Create.new(model_class, attr, raise_error: true, &block) register_action action end else action = Dynamoid::TransactionWrite::Create.new(model_class, attributes, raise_error: true, &block) register_action action end end |
#delete(model_or_model_class, hash_key = nil, range_key = nil) ⇒ Dynamoid::Document
Delete a model.
Can be called either with a model:
Dynamoid::TransactionWrite.execute do |t|
t.delete(user)
end
or with a primary key:
Dynamoid::TransactionWrite.execute do |t|
t.delete(User, user_id)
end
Raises Dynamoid::Errors::MissingHashKey if a partition key has value nil and raises Dynamoid::Errors::MissingRangeKey if a sort key is required but has value nil.
There are the following differences between transactional and non-transactional #delete: TBD
-
transactional
#deletedoesn’t supportlock_versionattribute so it will not be incremented and will not be checked to detect concurrent modification of a model andDynamoid::Errors::StaleObjectErrorexception will not be raised -
transactional
#deletedoesn’t disassociate a model from associated ones if there is any
600 601 602 603 604 605 606 607 |
# File 'lib/dynamoid/transaction_write.rb', line 600 def delete(model_or_model_class, hash_key = nil, range_key = nil) action = if model_or_model_class.is_a? Class Dynamoid::TransactionWrite::DeleteWithPrimaryKey.new(model_or_model_class, hash_key, range_key) else Dynamoid::TransactionWrite::DeleteWithInstance.new(model_or_model_class) end register_action action end |
#destroy(model) ⇒ Dynamoid::Document
Delete a model.
Runs callbacks.
Raises Dynamoid::Errors::MissingHashKey if a partition key has value nil and raises Dynamoid::Errors::MissingRangeKey if a sort key is required but has value nil.
There are the following differences between transactional and non-transactional #destroy:
-
transactional
#destroydoesn’t supportlock_versionattribute so it will not be incremented and will not be checked to detect concurrent modification of a model andDynamoid::Errors::StaleObjectErrorexception will not be raised -
transactional
#destroydoesn’t disassociate a model from associated ones if there are association declared in the model class
655 656 657 658 |
# File 'lib/dynamoid/transaction_write.rb', line 655 def destroy(model) action = Dynamoid::TransactionWrite::Destroy.new(model, raise_error: false) register_action action end |
#destroy!(model) ⇒ Dynamoid::Document|false
Delete a model.
Runs callbacks.
Raises Dynamoid::Errors::RecordNotDestroyed exception if model deleting failed (e.g. aborted by a callback).
Raises Dynamoid::Errors::MissingHashKey if a partition key has value nil and raises Dynamoid::Errors::MissingRangeKey if a sort key is required but has value nil.
There are the following differences between transactional and non-transactional #destroy!:
-
transactional
#destroy!doesn’t supportlock_versionattribute so it will not be incremented and will not be checked to detect concurrent modification of a model andDynamoid::Errors::StaleObjectErrorexception will not be raised -
transactional
#destroy!doesn’t disassociate a model from associated ones if there are association declared in the model class
631 632 633 634 |
# File 'lib/dynamoid/transaction_write.rb', line 631 def destroy!(model) action = Dynamoid::TransactionWrite::Destroy.new(model, raise_error: true) register_action action end |
#rollback ⇒ Object
148 149 150 |
# File 'lib/dynamoid/transaction_write.rb', line 148 def rollback run_on_rollback_callbacks end |
#save(model, **options) ⇒ true|false
Create new model or persist changes in already existing one.
Run the validation and callbacks. Raise Dynamoid::Errors::DocumentNotValid unless this object is valid.
user = User.new
Dynamoid::TransactionWrite.execute do |t|
t.save(user)
end
Validation can be skipped with validate: false option:
user = User.new(age: -1)
Dynamoid::TransactionWrite.execute do |t|
t.save(user, validate: false)
end
save by default sets timestamps attributes - created_at and updated_at when creates new model and updates updated_at attribute when updates already existing one.
If a model is new and hash key (id by default) is not assigned yet it was assigned implicitly with random UUID value.
When a model is not persisted - its id should have unique value. Otherwise a transaction will be rolled back.
Raises Dynamoid::Errors::MissingHashKey if a model is already persisted and a partition key has value nil and raises Dynamoid::Errors::MissingRangeKey if a sort key is required but has value nil.
There are the following differences between transactional and non-transactional #save:
-
transactional
#savedoesn’t support the:touchoption -
transactional
#savedoesn’t supportlock_versionattribute so it will not be incremented and will not be checked to detect concurrent modification of a model andDynamoid::Errors::StaleObjectErrorexception will not be raised -
transactional
#savedoesn’t raiseDynamoid::Errors::RecordNotUniqueat saving new model when primary key is already used. A genericAws::DynamoDB::Errors::TransactionCanceledExceptionis raised instead. -
transactional
savedoesn’t raiseDynamoid::Errors::StaleObjectErrorwhen a model that is being updated was concurrently deleted -
a table isn’t created lazily if it doesn’t exist yet
262 263 264 265 |
# File 'lib/dynamoid/transaction_write.rb', line 262 def save(model, **) action = Dynamoid::TransactionWrite::Save.new(model, **, raise_error: false) register_action action end |
#save!(model, **options) ⇒ true|false
Create new model or persist changes in already existing one.
Run the validation and callbacks. Returns true if saving is successful and false otherwise.
user = User.new
Dynamoid::TransactionWrite.execute do |t|
t.save!(user)
end
Validation can be skipped with validate: false option:
user = User.new(age: -1)
Dynamoid::TransactionWrite.execute do |t|
t.save!(user, validate: false)
end
save! by default sets timestamps attributes - created_at and updated_at when creates new model and updates updated_at attribute when updates already existing one.
If a model is new and hash key (id by default) is not assigned yet it was assigned implicitly with random UUID value.
When a model is not persisted - its id should have unique value. Otherwise a transaction will be rolled back.
Raises Dynamoid::Errors::MissingHashKey if a model is already persisted and a partition key has value nil and raises Dynamoid::Errors::MissingRangeKey if a sort key is required but has value nil.
There are the following differences between transactional and non-transactional #save!:
-
transactional
#save!doesn’t support the:touchoption -
transactional
#save!doesn’t supportlock_versionattribute so it will not be incremented and will not be checked to detect concurrent modification of a model andDynamoid::Errors::StaleObjectErrorexception will not be raised -
transactional
#save!doesn’t raiseDynamoid::Errors::RecordNotUniqueat saving new model when primary key is already used. A genericAws::DynamoDB::Errors::TransactionCanceledExceptionis raised instead. -
transactional
save!doesn’t raiseDynamoid::Errors::StaleObjectErrorwhen a model that is being updated was concurrently deleted -
a table isn’t created lazily if it doesn’t exist yet
205 206 207 208 |
# File 'lib/dynamoid/transaction_write.rb', line 205 def save!(model, **) action = Dynamoid::TransactionWrite::Save.new(model, **, raise_error: true) register_action action end |
#update_attributes(model, attributes) ⇒ true|false
Update multiple attributes at once.
Dynamoid::TransactionWrite.execute do |t|
t.update_attributes(user, age: 27, last_name: 'Tylor')
end
Returns true if saving is successful and false otherwise.
Raises Dynamoid::Errors::MissingHashKey if a partition key has value nil and raises Dynamoid::Errors::MissingRangeKey if a sort key is required but has value nil.
There are the following differences between transactional and non-transactional #update_attributes:
-
transactional
#update_attributesdoesn’t supportlock_versionattribute so it will not be incremented and will not be checked to detect concurrent modification of a model andDynamoid::Errors::StaleObjectErrorexception will not be raised -
transactional
update_attributesdoesn’t raiseDynamoid::Errors::StaleObjectErrorwhen a model that is being updated was concurrently deleted -
a table isn’t created lazily if it doesn’t exist yet
530 531 532 533 |
# File 'lib/dynamoid/transaction_write.rb', line 530 def update_attributes(model, attributes) action = Dynamoid::TransactionWrite::UpdateAttributes.new(model, attributes, raise_error: false) register_action action end |
#update_attributes!(model, attributes) ⇒ Object
Update multiple attributes at once.
Returns true if saving is successful and false otherwise.
Dynamoid::TransactionWrite.execute do |t|
t.update_attributes(user, age: 27, last_name: 'Tylor')
end
Raises a Dynamoid::Errors::DocumentNotValid exception if some vaidation fails.
Raises Dynamoid::Errors::MissingHashKey if a partition key has value nil and raises Dynamoid::Errors::MissingRangeKey if a sort key is required but has value nil.
There are the following differences between transactional and non-transactional #update_attributes!:
-
transactional
#update_attributes!doesn’t supportlock_versionattribute so it will not be incremented and will not be checked to detect concurrent modification of a model andDynamoid::Errors::StaleObjectErrorexception will not be raised -
transactional
update_attributes!doesn’t raiseDynamoid::Errors::StaleObjectErrorwhen a model that is being updated was concurrently deleted -
a table isn’t created lazily if it doesn’t exist yet
564 565 566 567 |
# File 'lib/dynamoid/transaction_write.rb', line 564 def update_attributes!(model, attributes) action = Dynamoid::TransactionWrite::UpdateAttributes.new(model, attributes, raise_error: true) register_action action end |
#update_fields(model_class, hash_key, range_key = nil, attributes = nil, &block) ⇒ nil
Update document.
Doesn’t run validations and callbacks.
Dynamoid::TransactionWrite.execute do |t|
t.update_fields(User, '1', age: 26)
end
If range key is declared for a model it should be passed as well:
Dynamoid::TransactionWrite.execute do |t|
t.update_fields(User, '1', 'Tylor', age: 26)
end
Updates can also be performed in a block.
Dynamoid::TransactionWrite.execute do |t|
t.update_fields(User, 1) do |u|
u.add(article_count: 1)
u.delete(favorite_colors: 'green')
u.set(age: 27, last_name: 'Tylor')
end
end
Operation add just adds a value for numeric attributes and join collections if attribute is a set.
t.update_fields(User, 1) do |u|
u.add(age: 1, followers_count: 5)
u.add(hobbies: ['skying', 'climbing'])
end
Operation delete is applied to collection attribute types and substructs one collection from another.
t.update_fields(User, 1) do |u|
u.delete(hobbies: ['skying'])
end
Operation set just changes an attribute value:
t.update_fields(User, 1) do |u|
u.set(age: 21)
end
Operation remove removes one or more attributes from an item.
t.update_fields(User, 1) do |u|
u.remove(:age)
end
All the operations work like ADD, DELETE, REMOVE, and SET actions supported by UpdateExpression parameter of UpdateItem operation.
It’s atomic operations. So adding or deleting elements in a collection or incrementing or decrementing a numeric field is atomic and does not interfere with other write requests.
Raises a Dynamoid::Errors::UnknownAttribute exception if any of the attributes is not declared in the model class.
Raises Dynamoid::Errors::MissingHashKey if a partition key has value nil and Dynamoid::Errors::MissingRangeKey if a sort key is required but has value nil.
There are the following differences between transactional and non-transactional #update_fields:
-
transactional
#update_fieldsdoesn’t support conditions (that’sifandunless_existsoptions) -
transactional
#update_fieldsdoesn’t return a document that was updated or created
492 493 494 495 496 497 498 499 500 501 |
# File 'lib/dynamoid/transaction_write.rb', line 492 def update_fields(model_class, hash_key, range_key = nil, attributes = nil, &block) # given no attributes, but there may be a block if range_key.is_a?(Hash) && !attributes attributes = range_key range_key = nil end action = Dynamoid::TransactionWrite::UpdateFields.new(model_class, hash_key, range_key, attributes, &block) register_action action end |
#upsert(model_class, hash_key, range_key = nil, attributes) ⇒ nil
Update an existing document or create a new one.
If a document with specified hash and range keys doesn’t exist it creates a new document with specified attributes. Doesn’t run validations and callbacks.
Dynamoid::TransactionWrite.execute do |t|
t.upsert(User, '1', age: 26)
end
If range key is declared for a model it should be passed as well:
Dynamoid::TransactionWrite.execute do |t|
t.upsert(User, '1', 'Tylor', age: 26)
end
Raises a Dynamoid::Errors::UnknownAttribute exception if any of the attributes is not declared in the model class.
Raises Dynamoid::Errors::MissingHashKey if a partition key has value nil and Dynamoid::Errors::MissingRangeKey if a sort key is required but has value nil.
There are the following differences between transactional and non-transactional #upsert:
-
transactional
#upsertdoesn’t support conditions (that’sifandunless_existsoptions) -
transactional
#upsertdoesn’t return a document that was updated or created
408 409 410 411 |
# File 'lib/dynamoid/transaction_write.rb', line 408 def upsert(model_class, hash_key, range_key = nil, attributes) # rubocop:disable Style/OptionalArguments action = Dynamoid::TransactionWrite::Upsert.new(model_class, hash_key, range_key, attributes) register_action action end |