Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Review transactions #813

Open
wants to merge 30 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7e958e6
Add specs roll backs when an issue is detected on the DynamoDB's side
andrykonchin Oct 6, 2024
a4ddecb
Remove :skip_existence_check option
andrykonchin Oct 6, 2024
55a3b28
Remove :skip_callbacks and :skip_validation options
andrykonchin Oct 6, 2024
198f408
Avoid not documented ways to pass parameters in specs
andrykonchin Oct 6, 2024
3d7ef2f
Refactor and add missing specs for transactions
andrykonchin Oct 6, 2024
a94b496
Add some missing specs in spec/dynamoid/persistence_spec.rb
andrykonchin Oct 17, 2024
8bde9f9
Rename #update(model, attributes) into #update_attributes(model, attr…
andrykonchin Oct 28, 2024
b017967
Replace #update(class, attributes) with #update_fields(class, hash_ke…
andrykonchin Oct 28, 2024
1abc903
Remove not supported use cases in README_transact.md
andrykonchin Oct 28, 2024
2eb9ae8
Refactor how transactions are implemented
andrykonchin Oct 30, 2024
711db0c
Restore commented specs
andrykonchin Oct 31, 2024
b14471e
Fix model updating when no attributes are specified and no timestamps…
andrykonchin Oct 31, 2024
768827c
Fix #destroy and return false when execution aborted
andrykonchin Oct 31, 2024
5171c47
Fix #destroy! and raise RecordNotDestroyed error when execution aborted
andrykonchin Oct 31, 2024
64d32b5
Adjust #destroy/#destroy! to not raise error when an item doesn't exi…
andrykonchin Oct 31, 2024
4637491
Adjust #create/#save/#update_attributes to handle aborting and item n…
andrykonchin Nov 1, 2024
3ead29e
Add missing specs to check that invalid model after persisting still …
andrykonchin Nov 1, 2024
11aa9af
Add missing specs for #changed? and #persisted? predicated
andrykonchin Nov 1, 2024
d974567
Change #upsert signature to accept hash and range keys parameters sep…
andrykonchin Nov 1, 2024
41232c2
Change #delete signature to accept hash and range keys parameters sep…
andrykonchin Nov 1, 2024
6ad9e13
Validate primary key in #save
andrykonchin Nov 1, 2024
3f7e154
Support #save(object, validate: false)
andrykonchin Nov 1, 2024
463360d
Persist multiple models in #create/#create!
andrykonchin Nov 3, 2024
c317a80
Accept a block in #create/#create!
andrykonchin Nov 3, 2024
c530359
Accept touch: false option in #save/#save!
andrykonchin Nov 3, 2024
963aa43
Assign a model id immediately in #save/#save! so it can be used in ot…
andrykonchin Nov 3, 2024
6b410fb
Polish documentation for transactions
andrykonchin Nov 3, 2024
1e7d035
Move the documentation for transactions into the main README.md
andrykonchin Nov 3, 2024
eba2480
Skip specs for callback aborting on Rails 4.2
andrykonchin Nov 3, 2024
7208702
Fix Rubocop warnings
andrykonchin Nov 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 131 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1046,10 +1046,138 @@ resolving the fields with a second query against the table since a query
against GSI then a query on base table is still likely faster than scan
on the base table*

### Transaction Writes
### Transactions in Dynamoid

Multiple write actions can be grouped together and submitted as an all-or-nothing operation.
See the [transation documentation](README_transact.md).
Multiple modifying actions can be grouped together and submitted as an
all-or-nothing operation. Atomic modifying operations are supported in
Dynamoid using transactions. If any action in the transaction fails they
all fail.

The following actions are supported:

* `#create` - add a new model if it does not already exist
* `#save` - create or update model
* `#update_attributes` - modifies one or more attributes from an existig
model
* `#delete` - remove an model without callbacks nor validations
* `#destroy` - remove an model
* `#upsert` - add a new model or update an existing one, no callbacks
* `#update_fields` - update a model without its instantiation

These methods are supposed to behave exactly like their
non-transactional counterparts.


#### Create models

Models can be created inside of a transaction. The partition and sort
keys, if applicable, are used to determine uniqueness. Creating will
fail with `Aws::DynamoDB::Errors::TransactionCanceledException` if a
model already exists.

This example creates a user with a unique id and unique email address by
creating 2 models. An additional model is upserted in the same
transaction. Upsert will update `updated_at` but will not create
`created_at`.

```ruby
user_id = SecureRandom.uuid
email = '[email protected]'

Dynamoid::TransactionWrite.execute do |txn|
txn.create(User, id: user_id)
txn.create(UserEmail, id: "UserEmail##{email}", user_id: user_id)
txn.create(Address, id: 'A#2', street: '456')
txn.upsert(Address, id: 'A#1', street: '123')
end
```

#### Save models

Models can be saved in a transaction. New records are created otherwise
the model is updated. Save, create, update, validate and destroy
callbacks are called around the transaction as appropriate. Validation
failures will throw `Dynamoid::Errors::DocumentNotValid`.

```ruby
user = User.find(1)
article = Article.new(body: 'New article text', user_id: user.id)

Dynamoid::TransactionWrite.execute do |txn|
txn.save(article)

user.last_article_id = article.id
txn.save(user)
end
```

#### Update models

A model can be updated by providing a model or primary key, and the fields to update.

```ruby
Dynamoid::TransactionWrite.execute do |txn|
# change name and title for a user
txn.update_attributes(user, name: 'bob', title: 'mister')

# sets the name and title for a user
# The user is found by id (that equals 1)
txn.update_fields(User, '1', name: 'bob', title: 'mister')
end
```

#### Destroy or delete models

Models can be used or the model class and key can be specified.
`#destroy` uses callbacks and validations. Use `#delete` to skip
callbacks and validations.

```ruby
article = Article.find('1')
tag = article.tag

Dynamoid::TransactionWrite.execute do |txn|
txn.destroy(article)
txn.delete(tag)

txn.delete(Tag, '2') # delete record with hash key '2' if it exists
txn.delete(Tag, 'key#abcd', 'range#1') # when sort key is required
end
```

#### Validation failures that don't raise

All of the transaction methods can be called without the `!` which
results in `false` instead of a raised exception when validation fails.
Ignoring validation failures can lead to confusion or bugs so always
check return status when not using a method with `!`.

```ruby
user = User.find('1')
user.red = true

Dynamoid::TransactionWrite.execute do |txn|
if txn.save(user) # won't raise validation exception
txn.update(UserCount, id: 'UserCount#Red', count: 5)
else
puts 'ALERT: user not valid, skipping'
end
end
```

#### Incrementally building a transaction

Transactions can also be built without a block.

```ruby
transaction = Dynamoid::TransactionWrite.new

transaction.create(User, id: user_id)
transaction.create(UserEmail, id: "UserEmail##{email}", user_id: user_id)
transaction.upsert(Address, id: 'A#1', street: '123')

transaction.commit # changes are persisted in this moment
```

### PartiQL

Expand Down
144 changes: 0 additions & 144 deletions README_transact.md

This file was deleted.

11 changes: 10 additions & 1 deletion lib/dynamoid/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,20 @@ def initialize(item)
end
end

class RecordNotSaved < Error
attr_reader :record

def initialize(record)
super('Failed to save the item')
@record = record
end
end

class RecordNotDestroyed < Error
attr_reader :record

def initialize(record)
super('Failed to destroy item')
super('Failed to destroy the item')
@record = record
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/dynamoid/loadable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def load(attrs)

self
end
alias assign_attributes load

# Reload an object from the database -- if you suspect the object has changed in the data store and you need those
# changes to be reflected immediately, you would call this method. This is a consistent read.
Expand Down
Loading
Loading