Skip to content

Commit 312421b

Browse files
authored
Merge pull request #813 from Dynamoid/ak/review-transactions
Review transactions
2 parents d0662db + f9a4994 commit 312421b

36 files changed

+5507
-1709
lines changed

README.md

Lines changed: 135 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,10 +1064,142 @@ resolving the fields with a second query against the table since a query
10641064
against GSI then a query on base table is still likely faster than scan
10651065
on the base table*
10661066

1067-
### Transaction Writes
1067+
### Transactions in Dynamoid
10681068

1069-
Multiple write actions can be grouped together and submitted as an all-or-nothing operation.
1070-
See the [transation documentation](README_transact.md).
1069+
> [!WARNING]
1070+
> Please note that this API is experimental and can be changed in
1071+
> future releases.
1072+
1073+
Multiple modifying actions can be grouped together and submitted as an
1074+
all-or-nothing operation. Atomic modifying operations are supported in
1075+
Dynamoid using transactions. If any action in the transaction fails they
1076+
all fail.
1077+
1078+
The following actions are supported:
1079+
1080+
* `#create`/`#create!` - add a new model if it does not already exist
1081+
* `#save`/`#save!` - create or update model
1082+
* `#update_attributes`/`#update_attributes!` - modifies one or more attributes from an existig
1083+
model
1084+
* `#delete` - remove an model without callbacks nor validations
1085+
* `#destroy`/`#destroy!` - remove an model
1086+
* `#upsert` - add a new model or update an existing one, no callbacks
1087+
* `#update_fields` - update a model without its instantiation
1088+
1089+
These methods are supposed to behave exactly like their
1090+
non-transactional counterparts.
1091+
1092+
1093+
#### Create models
1094+
1095+
Models can be created inside of a transaction. The partition and sort
1096+
keys, if applicable, are used to determine uniqueness. Creating will
1097+
fail with `Aws::DynamoDB::Errors::TransactionCanceledException` if a
1098+
model already exists.
1099+
1100+
This example creates a user with a unique id and unique email address by
1101+
creating 2 models. An additional model is upserted in the same
1102+
transaction. Upsert will update `updated_at` but will not create
1103+
`created_at`.
1104+
1105+
```ruby
1106+
user_id = SecureRandom.uuid
1107+
1108+
1109+
Dynamoid::TransactionWrite.execute do |txn|
1110+
txn.create(User, id: user_id)
1111+
txn.create(UserEmail, id: "UserEmail##{email}", user_id: user_id)
1112+
txn.create(Address, id: 'A#2', street: '456')
1113+
txn.upsert(Address, 'A#1', street: '123')
1114+
end
1115+
```
1116+
1117+
#### Save models
1118+
1119+
Models can be saved in a transaction. New records are created otherwise
1120+
the model is updated. Save, create, update, validate and destroy
1121+
callbacks are called around the transaction as appropriate. Validation
1122+
failures will throw `Dynamoid::Errors::DocumentNotValid`.
1123+
1124+
```ruby
1125+
user = User.find(1)
1126+
article = Article.new(body: 'New article text', user_id: user.id)
1127+
1128+
Dynamoid::TransactionWrite.execute do |txn|
1129+
txn.save(article)
1130+
1131+
user.last_article_id = article.id
1132+
txn.save(user)
1133+
end
1134+
```
1135+
1136+
#### Update models
1137+
1138+
A model can be updated by providing a model or primary key, and the fields to update.
1139+
1140+
```ruby
1141+
Dynamoid::TransactionWrite.execute do |txn|
1142+
# change name and title for a user
1143+
txn.update_attributes(user, name: 'bob', title: 'mister')
1144+
1145+
# sets the name and title for a user
1146+
# The user is found by id (that equals 1)
1147+
txn.update_fields(User, '1', name: 'bob', title: 'mister')
1148+
end
1149+
```
1150+
1151+
#### Destroy or delete models
1152+
1153+
Models can be used or the model class and key can be specified.
1154+
`#destroy` uses callbacks and validations. Use `#delete` to skip
1155+
callbacks and validations.
1156+
1157+
```ruby
1158+
article = Article.find('1')
1159+
tag = article.tag
1160+
1161+
Dynamoid::TransactionWrite.execute do |txn|
1162+
txn.destroy(article)
1163+
txn.delete(tag)
1164+
1165+
txn.delete(Tag, '2') # delete record with hash key '2' if it exists
1166+
txn.delete(Tag, 'key#abcd', 'range#1') # when sort key is required
1167+
end
1168+
```
1169+
1170+
#### Validation failures that don't raise
1171+
1172+
All of the transaction methods can be called without the `!` which
1173+
results in `false` instead of a raised exception when validation fails.
1174+
Ignoring validation failures can lead to confusion or bugs so always
1175+
check return status when not using a method with `!`.
1176+
1177+
```ruby
1178+
user = User.find('1')
1179+
user.red = true
1180+
1181+
Dynamoid::TransactionWrite.execute do |txn|
1182+
if txn.save(user) # won't raise validation exception
1183+
txn.update_fields(UserCount, user.id, count: 5)
1184+
else
1185+
puts 'ALERT: user not valid, skipping'
1186+
end
1187+
end
1188+
```
1189+
1190+
#### Incrementally building a transaction
1191+
1192+
Transactions can also be built without a block.
1193+
1194+
```ruby
1195+
transaction = Dynamoid::TransactionWrite.new
1196+
1197+
transaction.create(User, id: user_id)
1198+
transaction.create(UserEmail, id: "UserEmail##{email}", user_id: user_id)
1199+
transaction.upsert(Address, 'A#1', street: '123')
1200+
1201+
transaction.commit # changes are persisted in this moment
1202+
```
10711203

10721204
### PartiQL
10731205

README_transact.md

Lines changed: 0 additions & 144 deletions
This file was deleted.

dynamoid.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Gem::Specification.new do |spec|
2929

3030
spec.description = "Dynamoid is an ORM for Amazon's DynamoDB that supports offline development, associations, querying, and everything else you'd expect from an ActiveRecord-style replacement."
3131
spec.summary = "Dynamoid is an ORM for Amazon's DynamoDB"
32-
# Ignore not commited files
32+
# Ignore not committed files
3333
spec.files = Dir[
3434
'CHANGELOG.md',
3535
'dynamoid.gemspec',

lib/dynamoid/components.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module Components
1313

1414
define_model_callbacks :create, :save, :destroy, :update
1515
define_model_callbacks :initialize, :find, :touch, only: :after
16+
define_model_callbacks :commit, :rollback, only: :after
1617

1718
before_save :set_expires_field
1819
after_initialize :set_inheritance_field

lib/dynamoid/errors.rb

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,20 @@ def initialize(item)
2323
end
2424
end
2525

26+
class RecordNotSaved < Error
27+
attr_reader :record
28+
29+
def initialize(record)
30+
super('Failed to save the item')
31+
@record = record
32+
end
33+
end
34+
2635
class RecordNotDestroyed < Error
2736
attr_reader :record
2837

2938
def initialize(record)
30-
super('Failed to destroy item')
39+
super('Failed to destroy the item')
3140
@record = record
3241
end
3342
end
@@ -80,5 +89,7 @@ class UnsupportedKeyType < Error; end
8089
class UnknownAttribute < Error; end
8190

8291
class SubclassNotFound < Error; end
92+
93+
class Rollback < Error; end
8394
end
8495
end

lib/dynamoid/loadable.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ def load(attrs)
1111

1212
self
1313
end
14+
alias assign_attributes load
1415

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

0 commit comments

Comments
 (0)