.. _transactions: ############ Transactions ############ .. currentmodule:: dynamodb_mapper.transactions The :ref:`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. Transaction concepts ==================== 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 :py:meth:`~.Transaction._setup` which will be called before any transactors. Sub-transactions, if applicable, are ran after the main transactors if they all succeeded. Hence, :py:meth:`~.Transaction._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. Using the transaction engine ============================ 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. Use case: Bundle purchase ------------------------- :: 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. Use case: PowerUp purchase -------------------------- 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 - pre-load the ``Bundle`` - dynamically use the content in get_transactors - save the detailed status in a specially overloaded Transaction's ``__schema__`` 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 :py:meth:`.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. Related exceptions ================== MaxRetriesExceededError ----------------------- .. autoclass:: dynamodb_mapper.model.MaxRetriesExceededError Note: ``MAX_RETRIES`` is currently hardcoded to ``100`` in transactions module. TargetNotFoundError ------------------- .. autoclass:: dynamodb_mapper.transactions.TargetNotFoundError