The save use case demonstrates the use of raise_on_conflict argument. What it does is actually implement by hand a transaction. Amazon’s DynamoDB has no “out of the box” transaction engines but provides this parameter as an elementary block for this purpose.
Transactions are a convenient way to logically group database operations while trying as much as possible to enforce consistency. In Dynamodb-mapper, transactions are plain DynamoDBModel thus allowing them to persist their state. Dynamodb-mapper provides 2 grouping level: Targets and sub-transactions.
Transactions operates on a list of ‘targets’. For each target, it needs list of transactors. transactors are tuples of (getter, setter). The getter is responsible of either getting a fresh copy of the target either create it while setter performs the modifications. The call to save is handled by the engine itself.
For each target, the transaction engine will successively call getter and setter until save() succeeds. save() will succeed if and only if the target has not been altered by another thread in the mean time thus avoiding the lost update syndrome.
Optionally, transactions may define a method _setup() which will be called before any transactors.
Sub-transactions, if applicable, are ran after the main transactors if they all succeeded. Hence, _setup() and the transactors may dynamically append sub-transactions to the main transactions.
Unless the transaction is explicitely marked transient, its state will be persisted to a dedicated table. Transaction base class embeds a minimal schema that should suit most applications but may be overloaded as long as a datetime range_key is preserved along with a unicode status field.
Since version 1.7.0, transactions may operate on new (not yet persisted) Items.
To use the transaction engine, all you have to do is to define __table__ and overload _get_transactors(). Of course the transactors will themselves will need to be implemented. Optionally, you may overload the whole schema or set transient=True. A _setup() method may also be implemented.
During the transaction itself, please set requester_id field to any relevant interger unless the transaction is transient. _setup() is a good place to do it.
Note: transient flag may be toggled on a per instance basis. It may even be toggled in one of the transactors.
from dynamodb_mapper.transactions import Transaction, TargetNotFoundError
# define PlayerExperience, PlayerPowerUp, PlayerSkins, Players with user_id as hash_key
class InsufficientResourceError(Exception):
pass
bundle = {
u"cost": 150,
u"items": [
PlayerExperience,
PlayerPowerUp,
PlayerSkins
]
}
class BundleTransaction(Transaction):
transient = False # Make it explicit. This is anyway the default.
__table__ = u"mygame-dev-bundletransactions"
def __init__(self, user_id, bundle):
super(BundleTransaction, self).__init__()
self.requester_id = user_id
self.bundle = bundle
# _setup() is not needed here
def _get_transactors(self):
transactors = [(
lambda: Players.get(self.requester_id), # lambda
self.user_payment # regular callback
)]
for Item in self.bundle.items:
transactors.append((
lambda: Item.get(self.requester_id),
lambda item: item.do_stuff()
))
return transactors
def user_payment(self, player):
if player.balance < self.bundle.cost:
raise InsufficientResourceError()
player.balance -= self.bundle.cost
# Run the transaction
try:
transaction = BundleTransaction(42, bundle)
transaction.commit()
except InsufficientResourceError:
print "Ooops, user {} has not enough coins to proceed...".format(42)
#That's it !
This example has been kept simple on purpose. In a real world application, you certainly would not model your data this way ! You can notice the power of this approach that is compatible with lambda niceties as well as regular callbacks.
This example is a bit more subtle than the previous one. The customer may purchase a ‘surprise‘ bundle of powerups. The database knows what is in the pack while the client application does not. As bundles may change from time to time, we want to log what exactly was purchased. Also, the actual PowerUp registration should not start until the Coins transaction has succeeded.
To reach this goal, we could
But that’s more hand work.
A much better way is to split the transaction into PowerupTransaction and UserPowerupTransaction. The former handles the coins and the registration of the sub-transaction while the later handles the PowerUo magic.
from dynamodb_mapper.transactions import Transaction, TargetNotFoundError
# define PlayerPowerUp, Players with user_id as hash_key
class InsufficientResourceError(Exception):
pass
# Sub-Transaction of PowerupTransaction. Will have i's own status
class UserPowerupTransaction(transaction):
__table__ = u"mygame-dev-userpoweruptransactions"
def __init__(self, player, powerup):
super(UserPowerupTransaction, self).__init__()
self.requester_id = player.user_id
self.powerup = powerup
def _get_transactors(self):
return [(
lambda: PlayerPowerUp.get(self.requester_id, self.powerup),
do_stuff()
)]
# Main Transaction class. Will have it's own status
class PowerupTransaction(Transaction):
__table__ = u"mygame-dev-poweruptransactions"
cost = 150 # hard-coded cost for the demo
powerups = ["..."] # hard-coded powerups for the demo
def _get_transactors(self):
return [(
lambda: Players.get(self.requester_id),
self.user_payment
)]
def user_payment(self, player):
# Payment logic
if player.balance < self.cost:
raise InsufficientResourceError()
player.balance -= self.cost
# Register (overwrite) sub-transactions
self.subtransactions = []
for powerupName in self.powerups:
self.subtransactions.append = (player, powerupName)
# Run the transaction
try:
transaction = PowerupTransaction(requester_id=42)
transaction.commit()
except InsufficientResourceError:
print "Ooops, user {} has not enough coins to proceed...".format(42)
#That's it !
Note: In some special “real-World(tm)” situations, it may be necessary to modify the behavior of subtransactions. It is possible to overload the method Transaction._apply_subtransactions() for this purpose. Use case: sub-transactions have been automatically/randomly generated by the main transaction and the application needs to know wich one were generated or perform some other application specific tasks when applying.