Spring Python solves these problems with its TransactionTemplate
. This utility class makes it easy to wrap business methods with transactional functionality that solves all of the problems listed earlier. Spring Python makes it easy to wrap our existing business functions with the TransactionTemplate
using its @transactional
decorator.
transfer
function, and put into a Bank
class.from springpython.database.core import * from springpython.database.factory import * class Bank(object): def __init__(self, connectionFactory): self.factory = connectionFactory self.dt = DatabaseTemplate(self.factory) def transfer(self, transfer_amt, source_act, target_act): self.dt.execute(""" update ACCOUNT set BALANCE = BALANCE - %s where ACCOUNT_NUM = %s""", (transfer_amt, source_act)) self.dt.execute(""" update ACCOUNT set BALANCE = BALANCE + %s where ACCOUNT_NUM = %s""", (transfer_amt, target_act))
In this situation, we have stripped out all the hand-coded transaction code. Instead, we have the simple, concise business logic that defines a transfer
operation.
Please note, this version of the Bank application is NOT yet safe.
The steps are easily shown with the following sequence diagram:
transfer
method with transaction protection by using Spring Python's @transactional
decorator.from springpython.database.core import * from springpython.database.factory import * from springpython.database.transaction import * class Bank(object): def __init__(self, connectionFactory): self.factory = connectionFactory self.dt = DatabaseTemplate(self.factory) @transactional def transfer(self, transfer_amt, source_act, target_act): self.dt.execute(""" update ACCOUNT set BALANCE = BALANCE - %s where ACCOUNT_NUM = %s""", (transfer_amt, source_act)) self.dt.execute(""" update ACCOUNT set BALANCE = BALANCE + %s where ACCOUNT_NUM = %s""", (transfer_amt, target_act))
@transactional
is a decorator that uses a hidden instance of TransactionTemplate
to execute our transfer function inside a very robust version of the transaction pattern. Our interactions are better shown in the following diagram.
@transactional
wraps our transfer
function, when the user invokes transfer
, they are hitting the decorator first@transactional
passes all the context of the requested method call to its private instance of TransactionTemplate
TransactionTemplate
starts a transactionTransactionTemplate
then calls the original Bank transfer
functionBank
carries out its business, totally unaware it is inside a transaction Bank
hands control back to the TransactionTemplate
, which issues a commit
TransactionTemplate
hands control back to @transactional
, and finally back to the caller TransactionTemplate
is the caller of transfer
, it can easily handle any number of return statements. If an exception is raised anywhere inside transfer, TransactionTemplate
will rollback
instead of commit
.With this clean separation of concerns, we can work on the business code without having to worry about getting transactions right.
Our first version of the transaction pattern was simple and naïve. We then examined the list of issues with that pattern. TransactionTemplate
has a much more sophisticated pattern that handles these extra situations:
@transactional
is coded by default to start new transactions if one isn't currently in progress, and to join a transaction if one already exists. We will look at the other transactional options later in this chapterAnother integrity gap exists in our transfer
code. It lies somewhere between the transaction pattern and our business logic. Can you spot it?
There are no checks to make sure that we even have $10,000 to transfer! Also, there is no type of security check ensuring that we own either of these two accounts. We will address this deficiency later on, when filling in the transactional details.
We're not done yet. In order to have @transactional
do its job, we need to link it with a Transaction Manager through an AutoTransactionalObject
. The Transaction Manager provides @transactional
with a handle into the database to issue necessary commits and rollbacks. It also tracks the context of existing transactions and make appropriate decisions about when to start new transactions.
ConnectionFactoryTransactionManager
to @transaction
through an AutoTransactionalObject
.from springpython.database.transaction import * TransactionTemplateaboutclass BankAppConfig(PythonConfig): def __init__(self, factory): PythonConfig.__init__(self) self.factory = factory @Object def transactionalObject(self): return AutoTransactionalObject(self.tx_mgr()) @Object def tx_mgr(self): return ConnectionFactoryTransactionManager(self.factory) @Object def bank(self): return Bank(self.factory)
tx_mgr
defines our Transaction Manager, which uses an injected factory in order to perform the SQL transaction APIs. This is the same type of factory used by DatabaseTemplate. tx_mgr
tracks when transactions begin and end, providing the necessary services for TransactionTemplate
and @transactional
.transactionalObject
defines an instance of AutoTransactionalObject
, an IoC post processor. Its job is to find all instances of @transactional
and link them with the tx_mgr
. This is what empowers @transactional
to do its job of ensuring data integrity through SQL transactions. bank
class is our business class. transfer
.if __name__ == "__main__": from springpython.context import ApplicationContext ctx = ApplicationContext(BankAppConfig( Sqlite3ConnectionFactory("/path/to/sqlite3db"))) service = ctx.get_object("bank") bank.transfer(10000.0, "SAVINGS", "CHECKING")
We saw this diagram earlier in the book, as an illustration of the key principles behind Spring Python.
TransactionTemplate
represents a Portable Service Abstraction.
Our banking example has shown how to decorate some business logic with Spring Python's @transactional
in order to make the operation transactional. Throughout the example, we have repeatedly mentioned TransactionTemplate
.
In this section, we will use TransactionTemplate
directly instead of @transactional
. We will also explore how to do this with and without IoC configuration.
Bank
, replacing @transactional
with TransactionTemplate
.class Bank(object): def __init__(self, connectionFactory): self.factory = connectionFactory: self.dt = DatabaseTemplate(self.factory) self.tx_mgr = ConnectionFactoryTransactionManager(self.factory) self.tx_template = TransactionTemplate(self.tx_mgr) def transfer(self, transfer_amt, source_act, target_act): class TxDefinition(TransactionCallbackWithoutResult): def doInTransactionWithoutResult(s, status): self.dt.execute(""" update ACCOUNT set BALANCE = BALANCE - %s where ACCOUNT_NUM = %s""", (transfer_amt, source_act)) self.dt.execute(""" update ACCOUNT set BALANCE = BALANCE + %s where ACCOUNT_NUM = %s""", (transfer_amt, target_act)) self.tx_template.execute(TxDefinition())
This version of our Bank
shows two more attributes: tx_mgr
and tx_template
. These could be injected into our class, but we chose to inject the connection factory only.
doInTransactionWithoutResult
, which contains our business logic. self
passed into transfer
and self
passed into TxDefinition, doInTransactionWithoutResult
names its first argument s
. TransactionCallbackWithoutResult
as a base class. For return values, use TransactionCallback/doInTransaction
instead.from springpython.database.transaction import * class BankAppConfig(PythonConfig): def __init__(self, factory): PythonConfig.__init__(self) self.factory = factory @Object def bank(self): return Bank(self.factory)
if __name__ == "__main__": from springpython.context import ApplicationContext ctx = ApplicationContext(BankAppConfig( Sqlite3ConnectionFactory("/path/to/sqlite3db"))) service = ctx.get_object("bank") bank.transfer(10000.0, "SAVINGS", "CHECKING")
In this situation, our IoC configuration is pretty simple. We could code the application without it. Just remember: IoC provides useful assistance in things like testing, mocking, and being able to swap out key objects.
We begin by rewriting the startup script, so that it doesn't need any IoC container.
if __name__ == "__main__": service = Bank(Sqlite3ConnectionFactory("/path/to/sqlite3db")) bank.transfer(10000.0, "SAVINGS", "CHECKING")
Because our Bank
was written using simple constructor injection, there was no need to alter it in order to run it without a container. Since we are programmatically using TransactionTemplate
, there is no requirement to use the IoC container. This offers developers an opportunity to evaluate Spring Python purely for the transactional features without having to try out the IoC container at the same time.
But it is important to remember that multiple examples of the value of IoC have already been shown in this book, and many more are coming.
The Spring way includes giving developers options. In order to choose the right approach, here are some pros and cons:
|
|
programmatic |
|
If the cons for either of these solutions are not acceptable, there is a third choice: declaring transactions from inside the IoC container. This allows easy wrapping of business code with transactions without code tangling and without editing already existing code. We will demonstrate this later in the chapter. But first let's look at adding new functions.
So far, we have managed to build a bank that does one thing: transfer money. As with any software project, we typically have to grow the functionality. As we proceed with modifications to our Bank
, we want to add new transactions without risking existing ones. Spring Python's transaction management makes this very simple.
In this section, we will go back to our @transactional
Bank and add some new functionality.
withdraw
function and deposit
function from the transfer function.class Bank(object): def __init__(self, connectionFactory): self.factory = connectionFactory): self.dt = DatabaseTemplate(self.factory) def withdraw(self, amt, act): self.dt.execute(""" update ACCOUNT set BALANCE = BALANCE - %s where ACCOUNT_NUM = %s""", (amt, act)) def deposit(self, amt, act): self.dt.execute(""" update ACCOUNT set BALANCE = BALANCE + %s where ACCOUNT_NUM = %s""", (amt, act)) @transactional def transfer(self, transfer_amt, source_act, target_act): self.withdraw(transfer_amt, source_act) self.deposit(transfer_amt, target_act)
By moving the two SQL statements into separate functions, we have nicely defined transfer
as withdraw
followed by deposit
. We are now ready to offer these two new functions to our clients. Do you notice anything wrong with this? Are the functions safe transaction-wise by themselves? What if another banking operation tried to reuse these primitives?
withdraw
and deposit
methods with @transactional
.class Bank(object): def __init__(self, connectionFactory): self.factory = connectionFactory): self.dt = DatabaseTemplate(self.factory) @transactional def withdraw(self, amt, act): self.dt.execute(""" update ACCOUNT set BALANCE = BALANCE - %s where ACCOUNT_NUM = %s""", (amt, act)) @transactional def deposit(self, amt, act): self.dt.execute(""" update ACCOUNT set BALANCE = BALANCE + %s where ACCOUNT_NUM = %s""", (amt, act)) @transactional def transfer(self, transfer_amt, source_act, target_act): self.withdraw(transfer_amt, source_act) self.deposit(transfer_amt, target_act)
The only difference in our code is marking withdraw
and deposit
with @transactional
.
Bank
by doing some checks before and after executing the SQL.class Bank(object): def __init__(self, connectionFactory): self.factory = connectionFactory): self.dt = DatabaseTemplate(self.factory) @transactional(["PROPAGATION_SUPPORTS"]) def balance(self, act): return self.dt.queryForObject(""" SELECT BALANCE FROM ACCOUNT WHERE ACCOUNT_NUM = ?""", (act,), types.FloatType) @transactional def withdraw(self, amt, act): if (self.balance(act) > amt): rows = self.dt.execute(""" update ACCOUNT set BALANCE = BALANCE - %s where ACCOUNT_NUM = %s""", (amt, act)) if (rows == 0): raise Exception("Account %s does not exist." % act) else: raise Exception("Account %s has insufficient funds." % act) @transactional def deposit(self, amt, act): rows = self.dt.execute(""" update ACCOUNT set BALANCE = BALANCE + %s where ACCOUNT_NUM = %s""", (amt, act)) if (rows == 0) { raise Exception("Account %s does not exist." % act) @transactional def transfer(self, transfer_amt, source_act, target_act): self.withdraw(transfer_amt, source_act) self.deposit(transfer_amt, target_act)
We have made several changes that start to make our application look like a real bank. They are as follows:
balance
function that allows us to look up the balance for an account withdraw
function now checks the balance to make sure there is enough to withdraw withdraw
function verifies that a row of data was updated, confirming the withdrawn account is real deposit
function verifies that a row of data was updated, confirming that the deposited account is realAs discussed earlier, the ACID properties of transactions are as follows:
Regarding atomicity, we have already practiced defined the beginning and end points for transactions by using @transactional
and TransactionTemplate
.
Spring Python supports propagation. Earlier, we stated that the default policy of @transactional
is to start a new transaction (if none existed) and join an existing transaction (if one was already in progress). Spring Python conveniently lets us take the safe, atomic operations of withdraw
and deposit
, and combine them together into transfer
, without having to interact with the SQL transaction APIs at all.
We also created another function, balance
, to lookup the current balance of accounts. Since balance
performs no updates, it doesn't require a transaction when run by itself. However, when called upon by an existing transaction, we want it to join in as if it was part of the transaction. This is accomplished by providing @transactional
with a propagation override:
@transactional(["PROPAGATION_SUPPORTS"]) def balance(self, act): return self.dt.queryForObject(""" SELECT BALANCE FROM ACCOUNT WHERE ACCOUNT_NUM = ?""", (act,), types.FloatType)
@transactional
scans the list of transaction definitions. Currently, Spring Python supports the following definitions:
Property |
Description |
---|---|
|
A transaction is required. If a current one exists, join it. Otherwise, start a new one. This is the default for |
|
A transaction is not required. This code can run inside or outside a transaction. |
|
A transaction is required. If a current one exists, join it. Otherwise, raise an exception. |
|
A transaction is not allowed. If a current one exists, raise an exception. Otherwise, run the code. |
Spring Python provides incredibly useful transaction context management, transaction API handling, and allows us clean demarcation of transactions.
As better definitions are added to Python's database specification for things like isolation, Spring Python will add more options to support it. This will increase our ability to cleanly declare the exact type of transaction needed to wrap our code.
An important aspect of Spring Python is its non-invasive nature. This was demonstrated in great detail in the chapter that introduced aspect oriented programming. Spring Python provides a convenient, non-intrusive method interceptor that allows the demarcation of existing code.
This solves the problem mentioned earlier, where neither editing existing source code nor tangling our business logic with transaction management are acceptable.
Bank
class that has no transaction demarcation.class Bank(object): def __init__(self, connectionFactory): self.factory = connectionFactory: self.dt = DatabaseTemplate(self.factory) def balance(self, act): return self.dt.queryForObject(""" SELECT BALANCE FROM ACCOUNT WHERE ACCOUNT_NUM = ?""", (act,), types.FloatType) def withdraw(self, amt, act): if (self.balance(act) > amt): rows = self.dt.execute(""" update ACCOUNT set BALANCE = BALANCE - %s where ACCOUNT_NUM = %s""", (amt, act)) if (rows == 0): raise Exception("Account %s does not exist." % act) else: raise Exception("Account %s has insufficient funds." % act) def deposit(self, amt, act): rows = self.dt.execute(""" update ACCOUNT set BALANCE = BALANCE + %s where ACCOUNT_NUM = %s""", (amt, act)) if (rows == 0) { raise Exception("Account %s does not exist." % act) def transfer(self, transfer_amt, source_act, target_act): self.withdraw(transfer_amt, source_act) self.deposit(transfer_amt, target_act)
This Bank
class is identical to the previous one, except for the fact that there are no @transactional
decorators.
class BankAppConfig(PythonConfig): def __init__(self, factory): PythonConfig.__init__(self) self.factory = factory @Object def bank_target(self): return Bank(self.factory) @Object def tx_mgr(self): return ConnectionFactoryTransactionManager(self.factory) @Object def bank(self): tx_attrs = [] tx_attrs.append((".*transfer", ["PROPAGATION_REQUIRED"])) tx_attrs.append((".*withdraw", ["PROPAGATION_REQUIRED"])) tx_attrs.append((".*deposit", ["PROPAGATION_REQUIRED"])) tx_attrs.append((".*balance", ["PROPAGATION_SUPPORTS"])) return TransactionProxyFactoryObject(self.tx_mgr(), self.bank_target(), tx_attrs)
With this alternative configuration, we use TransactionProxyFactoryObject. This is an out-of-the-box AOP interceptor that Spring Python offers to automatically wrap certain functions with TransactionTemplate. It requires a transaction manager as well as the target object, our Bank. It also needs a list of tuples, with each tuple defining a regular expression for method matching as well as a list of transaction properties just like we plugged into @transactional earlier in this chapter.
What is the right choice: @transactional
or TransactionProxyFactoryObject?
@transactional
is clear, concise, and easy-to-read. Its biggest drawback is the requirement to edit existing code. If we own the code, then this shouldn't be a problem. TransactionProxyFactoryObject
is the best choice.Transactions are intrinsically tied to databases. Attempting to mock or stub this out would require an extreme amount of effort, and probably not be worth the effort. This is one area where I generally agree with testing against an actual database.
It is possible to use lightweight databases such as sqlite
for this effort, but it may be risky if this isn't the target platform for production. In fact, the best testing effort would be a properly setup test bed using the same version of database engine as production. The important point is that it is easy to create lightweight tests against something small such as sqlite
. This can confirm to the developers that things are working as expected. More extensive integration testing can be done with a production grade test-bed.