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 more...
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
permalink #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
permalink .execute ⇒ Object
[View source] [View on GitHub]
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
permalink #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 |
permalink #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.
294 295 296 297 298 299 300 301 302 303 304 |
# File 'lib/dynamoid/transaction_write.rb', line 294 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 |
permalink #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.
255 256 257 258 259 260 261 262 263 264 265 |
# File 'lib/dynamoid/transaction_write.rb', line 255 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 |
permalink #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
Raise MissingRangeKey
if a range key is declared but not passed as argument.
470 471 472 473 474 475 476 477 |
# File 'lib/dynamoid/transaction_write.rb', line 470 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 |
permalink #destroy(model) ⇒ Dynamoid::Document
Delete a model.
Runs callbacks.
499 500 501 502 |
# File 'lib/dynamoid/transaction_write.rb', line 499 def destroy(model) action = Dynamoid::TransactionWrite::Destroy.new(model, raise_error: false) register_action action end |
permalink #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).
488 489 490 491 |
# File 'lib/dynamoid/transaction_write.rb', line 488 def destroy!(model) action = Dynamoid::TransactionWrite::Destroy.new(model, raise_error: true) register_action action end |
permalink #rollback ⇒ Object
[View source] [View on GitHub]
148 149 150 |
# File 'lib/dynamoid/transaction_write.rb', line 148 def rollback run_on_rollback_callbacks end |
permalink #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.
223 224 225 226 |
# File 'lib/dynamoid/transaction_write.rb', line 223 def save(model, **) action = Dynamoid::TransactionWrite::Save.new(model, **, raise_error: false) register_action action end |
permalink #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.
185 186 187 188 |
# File 'lib/dynamoid/transaction_write.rb', line 185 def save!(model, **) action = Dynamoid::TransactionWrite::Save.new(model, **, raise_error: true) register_action action end |
permalink #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.
426 427 428 429 |
# File 'lib/dynamoid/transaction_write.rb', line 426 def update_attributes(model, attributes) action = Dynamoid::TransactionWrite::UpdateAttributes.new(model, attributes, raise_error: false) register_action action end |
permalink #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.
445 446 447 448 |
# File 'lib/dynamoid/transaction_write.rb', line 445 def update_attributes!(model, attributes) action = Dynamoid::TransactionWrite::UpdateAttributes.new(model, attributes, raise_error: true) register_action action end |
permalink #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.
403 404 405 406 407 408 409 410 411 412 |
# File 'lib/dynamoid/transaction_write.rb', line 403 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 |
permalink #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.
330 331 332 333 |
# File 'lib/dynamoid/transaction_write.rb', line 330 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 |