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/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, Save, UpdateAttributes, UpdateFields, Upsert

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeTransactionWrite

Returns a new instance of TransactionWrite.



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

def initialize
  @actions = []
end

Class Method Details

.executeObject



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/dynamoid/transaction_write.rb', line 108

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


133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/dynamoid/transaction_write.rb', line 133

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.

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:



293
294
295
296
297
298
299
300
301
302
303
# File 'lib/dynamoid/transaction_write.rb', line 293

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.

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:



254
255
256
257
258
259
260
261
262
263
264
# File 'lib/dynamoid/transaction_write.rb', line 254

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

Raise MissingRangeKey if a range key is declared but not passed as argument.

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:



417
418
419
420
421
422
423
424
# File 'lib/dynamoid/transaction_write.rb', line 417

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.

Parameters:

Returns:



446
447
448
449
# File 'lib/dynamoid/transaction_write.rb', line 446

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).

Parameters:

Returns:



435
436
437
438
# File 'lib/dynamoid/transaction_write.rb', line 435

def destroy!(model)
  action = Dynamoid::TransactionWrite::Destroy.new(model, raise_error: true)
  register_action action
end

#rollbackObject



147
148
149
# File 'lib/dynamoid/transaction_write.rb', line 147

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.

Parameters:

Options Hash (**options):

  • :validate (true|false)

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

Returns:

  • (true|false)

    Whether saving successful or not



222
223
224
225
# File 'lib/dynamoid/transaction_write.rb', line 222

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.

Parameters:

Options Hash (**options):

  • :validate (true|false)

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

Returns:

  • (true|false)

    Whether saving successful or not



184
185
186
187
# File 'lib/dynamoid/transaction_write.rb', line 184

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.

Parameters:

  • model (Dynamoid::Document)

    a model

  • attributes (Hash)

    a hash of attributes to update

Returns:

  • (true|false)

    Whether updating successful or not



373
374
375
376
# File 'lib/dynamoid/transaction_write.rb', line 373

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.

Parameters:

  • model (Dynamoid::Document)

    a model

  • attributes (Hash)

    a hash of attributes to update



392
393
394
395
# File 'lib/dynamoid/transaction_write.rb', line 392

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

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

Raises a Dynamoid::Errors::UnknownAttribute exception if any of the attributes is not declared in the model class.

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)


356
357
358
359
# File 'lib/dynamoid/transaction_write.rb', line 356

def update_fields(model_class, hash_key, range_key = nil, attributes) # rubocop:disable Style/OptionalArguments
  action = Dynamoid::TransactionWrite::UpdateFields.new(model_class, hash_key, range_key, attributes)
  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.

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)


329
330
331
332
# File 'lib/dynamoid/transaction_write.rb', line 329

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