Class: Dynamoid::TransactionWrite

Inherits:
Object
  • Object
show all
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! , 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! , 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! , 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

Constructor Details

#initializeTransactionWrite

Returns a new instance of TransactionWrite.



125
126
127
# File 'lib/dynamoid/transaction_write.rb', line 125

def initialize
  @actions = []
end

Class Method Details

.executeObject



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

#commitObject

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 #create doesn’t support lock_version attribute so it will not be incremented and will not be checked to detect concurrent modification of a model and Dynamoid::Errors::StaleObjectError exception will not be raised

  • transactional #create doesn’t raise Dynamoid::Errors::RecordNotUnique at saving new model when primary key is already used. A generic Aws::DynamoDB::Errors::TransactionCanceledException is raised instead.

  • a table isn’t created lazily if it doesn’t exist yet

Parameters:

  • model_class (Class)

    a model class which should be instantiated

  • attributes (Hash|Array<Hash>) (defaults to: {})

    attributes of a model

  • block (Proc)

    a block to process a model after initialization

Returns:



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 support lock_version attribute so it will not be incremented and will not be checked to detect concurrent modification of a model and Dynamoid::Errors::StaleObjectError exception will not be raised

  • transactional #create! doesn’t raise Dynamoid::Errors::RecordNotUnique at saving new model when primary key is already used. A generic Aws::DynamoDB::Errors::TransactionCanceledException is raised instead.

  • a table isn’t created lazily if it doesn’t exist yet

Parameters:

  • model_class (Class)

    a model class which should be instantiated

  • attributes (Hash|Array<Hash>) (defaults to: {})

    attributes of a model

  • block (Proc)

    a block to process a model after initialization

Returns:



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 #delete doesn’t support lock_version attribute so it will not be incremented and will not be checked to detect concurrent modification of a model and Dynamoid::Errors::StaleObjectError exception will not be raised

  • transactional #delete doesn’t disassociate a model from associated ones if there is any

Parameters:

  • model_or_model_class (Class|Dynamoid::Document)

    either model or model class

  • hash_key (Scalar value) (defaults to: nil)

    hash key value

  • range_key (Scalar value) (defaults to: nil)

    range key value (optional)

Returns:



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 #destroy doesn’t support lock_version attribute so it will not be incremented and will not be checked to detect concurrent modification of a model and Dynamoid::Errors::StaleObjectError exception will not be raised

  • transactional #destroy doesn’t disassociate a model from associated ones if there are association declared in the model class

Parameters:

Returns:



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 support lock_version attribute so it will not be incremented and will not be checked to detect concurrent modification of a model and Dynamoid::Errors::StaleObjectError exception will not be raised

  • transactional #destroy! doesn’t disassociate a model from associated ones if there are association declared in the model class

Parameters:

Returns:



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

#rollbackObject



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 #save doesn’t support the :touch option

  • transactional #save doesn’t support lock_version attribute so it will not be incremented and will not be checked to detect concurrent modification of a model and Dynamoid::Errors::StaleObjectError exception will not be raised

  • transactional #save doesn’t raise Dynamoid::Errors::RecordNotUnique at saving new model when primary key is already used. A generic Aws::DynamoDB::Errors::TransactionCanceledException is raised instead.

  • transactional save doesn’t raise Dynamoid::Errors::StaleObjectError when a model that is being updated was concurrently deleted

  • a table isn’t created lazily if it doesn’t exist yet

Parameters:

Options Hash (**options):

  • :validate (true|false)

    validate a model or not - true by default (optional)

Returns:

  • (true|false)

    Whether saving successful or not



262
263
264
265
# File 'lib/dynamoid/transaction_write.rb', line 262

def save(model, **options)
  action = Dynamoid::TransactionWrite::Save.new(model, **options, 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 :touch option

  • transactional #save! doesn’t support lock_version attribute so it will not be incremented and will not be checked to detect concurrent modification of a model and Dynamoid::Errors::StaleObjectError exception will not be raised

  • transactional #save! doesn’t raise Dynamoid::Errors::RecordNotUnique at saving new model when primary key is already used. A generic Aws::DynamoDB::Errors::TransactionCanceledException is raised instead.

  • transactional save! doesn’t raise Dynamoid::Errors::StaleObjectError when a model that is being updated was concurrently deleted

  • a table isn’t created lazily if it doesn’t exist yet

Parameters:

Options Hash (**options):

  • :validate (true|false)

    validate a model or not - true by default (optional)

Returns:

  • (true|false)

    Whether saving successful or not



205
206
207
208
# File 'lib/dynamoid/transaction_write.rb', line 205

def save!(model, **options)
  action = Dynamoid::TransactionWrite::Save.new(model, **options, 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_attributes doesn’t support lock_version attribute so it will not be incremented and will not be checked to detect concurrent modification of a model and Dynamoid::Errors::StaleObjectError exception will not be raised

  • transactional update_attributes doesn’t raise Dynamoid::Errors::StaleObjectError when a model that is being updated was concurrently deleted

  • a table isn’t created lazily if it doesn’t exist yet

Parameters:

  • model (Dynamoid::Document)

    a model

  • attributes (Hash)

    a hash of attributes to update

Returns:

  • (true|false)

    Whether updating successful or not



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 support lock_version attribute so it will not be incremented and will not be checked to detect concurrent modification of a model and Dynamoid::Errors::StaleObjectError exception will not be raised

  • transactional update_attributes! doesn’t raise Dynamoid::Errors::StaleObjectError when a model that is being updated was concurrently deleted

  • a table isn’t created lazily if it doesn’t exist yet

Parameters:

  • model (Dynamoid::Document)

    a model

  • attributes (Hash)

    a hash of attributes to update



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_fields doesn’t support conditions (that’s if and unless_exists options)

  • transactional #update_fields doesn’t return a document that was updated or created

Parameters:

  • model_class (Class)

    a model class

  • hash_key (Scalar value)

    hash key value

  • range_key (Scalar value) (defaults to: nil)

    range key value (optional)

  • attributes (Hash) (defaults to: nil)

Returns:

  • (nil)


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 #upsert doesn’t support conditions (that’s if and unless_exists options)

  • transactional #upsert doesn’t return a document that was updated or created

Parameters:

  • model_class (Class)

    a model class

  • hash_key (Scalar value)

    hash key value

  • range_key (Scalar value) (defaults to: nil)

    range key value (optional)

  • attributes (Hash)

Returns:

  • (nil)


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