Skip to content

Commit 2bd4dd1

Browse files
author
Dariusz Suchojad
committed
SESPRINGPYTHONPY-149: Sphinx docs - Transaction Management.
1 parent 9050616 commit 2bd4dd1

File tree

2 files changed

+293
-1
lines changed

2 files changed

+293
-1
lines changed

docs/sphinx/source/dao.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and over. The DatabaseTemplate resolves that by handling the plumbing of these
99
operations while leaving you in control of the part that matters the most,
1010
the SQL.
1111

12+
.. _dao-external-dependencies:
13+
1214
External dependencies
1315
+++++++++++++++++++++
1416

docs/sphinx/source/transaction.rst

Lines changed: 291 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,292 @@
11
Transaction Management
2-
======================
2+
======================
3+
4+
When writing a program with database operations, you may need to use
5+
transactions. Your code can get ugly, and it often becomes hard to read the
6+
business logic due to starting, committing, or rolling back for various reasons.
7+
Another risk is that some of the transaction management code you write will
8+
have all the necessary steps, while you may forget some important steps in
9+
others. Spring Python offers a key level of abstraction that can remove that
10+
burden and allow you to focus on the business logic.
11+
12+
External dependencies
13+
=====================
14+
15+
If you choose to use DatabaseTemplate along with Spring Python's support for
16+
transaction management you need to install :doc:`an appropriate SQL database driver <dao>`
17+
module. Depending on the IoC configuration format you're going to use you may
18+
also need to install one of its :ref:`documented dependencies <dao-external-dependencies>`.
19+
20+
Solutions requiring transactions
21+
================================
22+
23+
For simple transactions, you can embed them programmatically.
24+
25+
Seen anything like this before?::
26+
27+
def transfer(transfer_amount, source_account_num, target_account_num):
28+
conn = MySQLdb.connection("springpython", "springpython", "localhost", "springpython")
29+
cursor = conn.cursor()
30+
cursor.execute("update ACCOUNT set BALANCE = BALANCE - %s where ACCOUNT_NUM = %s", (transfer_amount, source_account_num))
31+
cursor.execute("update ACCOUNT set BALANCE = BALANCE + %s where ACCOUNT_NUM = %s", (transfer_amount, target_account_num))
32+
cursor.close()
33+
34+
This business method defines a transfer between bank accounts. Notice any
35+
issues here? What happens if the target account doesn't exist? What about
36+
transferring a negative balance? What if the transfer amount exceeded the
37+
source account's balance? All these things require checks, and if something
38+
is wrong the entire transfer must be aborted, or you find the first bank account
39+
leaking money.
40+
41+
To wrap this function transactionally, based on DB-2.0 API specifications,
42+
we'll add some checks. I have also completed some refactorings and utilized
43+
the *DatabaseTemplate* to clean up my database code::
44+
45+
from springpython.database import *
46+
from springpython.database.core import *
47+
import types
48+
class Bank:
49+
def __init__(self):
50+
self.factory = factory.MySQLConnectionFactory("springpython", "springpython", "localhost", "springpython")
51+
self.dt = DatabaseTemplate(self.factory)
52+
53+
def balance(self, account_num):
54+
results = self.dt.query_for_list("select BALANCE from ACCOUNT where ACCOUNT_NUM = %s", (account_num,))
55+
if len(results) != 1:
56+
raise InvalidBankAccount("There were %s accounts that matched %s." % (len(results), account_num))
57+
return results[0][0]
58+
59+
def checkForSufficientFunds(self, source_balance, amount):
60+
if source_balance < amount:
61+
raise InsufficientFunds("Account %s did not have enough funds to transfer %s" % (source_account_num, amount))
62+
63+
def withdraw(self, amount, source_account_num):
64+
self.checkForSufficientFunds(self.balance(source_account_num), amount)
65+
self.dt.execute("update ACCOUNT set BALANCE = BALANCE - %s where ACCOUNT_NUM = %s", (amount, source_account_num))
66+
67+
def deposit(self, amount, target_account_num):
68+
# Implicitly testing for valid account number
69+
self.balance(target_account_num)
70+
self.dt.execute("update ACCOUNT set BALANCE = BALANCE + %s where ACCOUNT_NUM = %s", (amount, target_account_num))
71+
72+
def transfer(self, transfer_amount, source_account_num, target_account_num):
73+
try:
74+
cursor = self.factory.getConnection().cursor() # DB-2.0 API spec says that creating a cursor implicitly starts a transaction
75+
self.withdraw(transfer_amount, source_account_num)
76+
self.deposit(transfer_amount, target_account_num)
77+
self.factory.getConnection().commit()
78+
cursor.close() # There wasn't anything in this cursor, but it is good to close an opened cursor
79+
except InvalidBankAccount, InsufficientFunds:
80+
self.factory.getConnection().rollback()
81+
82+
* This has some extra checks put in to protect from overdrafts and invalid accounts.
83+
* *DatabaseTemplate* removes our need to open and close cursors.
84+
* Unfortunately, we still have to tangle with them as well as the connection in
85+
order to handle transactions.
86+
87+
TransactionTemplate
88+
===================
89+
90+
We still have to deal with exceptions. What if another part of the code raised
91+
another exception that we didn't trap? It might escape our try-except block of
92+
code, and then our data could lose integrity. If we plug in the
93+
*TransactionTemplate*, we can really simplify this and also guarantee management
94+
of any exceptions.
95+
96+
The following code block shows swapping out manual transaction for
97+
*TransactionTemplate*::
98+
99+
from springpython.database.transaction import *
100+
101+
class Bank:
102+
def __init__(self):
103+
self.factory = factory.MySQLConnectionFactory("springpython", "springpython", "localhost", "springpython")
104+
self.dt = DatabaseTemplate(self.factory)
105+
self.txManager = ConnectionFactoryTransactionManager(self.factory)
106+
self.txTemplate = TransactionTemplate(self.txManager)
107+
108+
def transfer(self, transfer_amount, source_account_num, target_account_num):
109+
class txDefinition(TransactionCallbackWithoutResult):
110+
def doInTransactionWithoutResult(s, status):
111+
self.withdraw(transfer_amount, source_account_num)
112+
self.deposit(transfer_amount, target_account_num)
113+
try:
114+
self.txTemplate.execute(txDefinition())
115+
print "If you made it to here, then your transaction has already been committed."
116+
except InvalidBankAccount, InsufficientFunds:
117+
print "If you made it to here, then your transaction has already been rolled back."
118+
119+
* We changed the init function to setup a *TransactionManager* (based on
120+
ConnectionFactory) and also a *TransactionTemplate*.
121+
* We also rewrote the transfer function to generate a callback.
122+
123+
Now you don't have to deal with implicit cursors, commits, and rollbacks.
124+
Managing commits and rollbacks can really complicated especially when dealing
125+
with exceptions. By wrapping it into a nice callback, *TransactionTemplate* does
126+
the work for us, and lets us focus on business logic, while encouraging us to
127+
continue to define meaningful business logic errors.
128+
129+
@transactional
130+
==============
131+
132+
Another option is to use the @transactional decorator, and mark which methods
133+
should be wrapped in a transaction when called::
134+
135+
from springpython.database.transaction import *
136+
137+
class Bank:
138+
def __init__(self, connectionFactory):
139+
self.factory = connectionFactory
140+
self.dt = DatabaseTemplate(self.factory)
141+
142+
@transactional
143+
def transfer(self, transfer_amount, source_account_num, target_account_num):
144+
self.withdraw(transfer_amount, source_account_num)
145+
self.deposit(transfer_amount, target_account_num)
146+
147+
This needs to be wired together with a *TransactionManager* in an
148+
*ApplicationContext*. The following example shows a :doc:`PythonConfig <objects-pythonconfig>`
149+
with three objects:
150+
151+
* the bank
152+
* a *TransactionManager* (in this case *ConnectionFactoryTransactionManager*)
153+
* an *AutoTransactionalObject*, which checks all objects to see if they have
154+
*@transactional* methods, and if so, links them with the *TransactionManager*.
155+
156+
The name of the method (i.e. component name) for *AutoTransactionalObject* doesn't matter::
157+
158+
class DatabaseTxTestDecorativeTransactions(PythonConfig):
159+
def __init__(self, factory):
160+
super(DatabaseTxTestDecorativeTransactions, self).__init__()
161+
self.factory = factory
162+
163+
@Object
164+
def transactionalObject(self):
165+
return AutoTransactionalObject(self.tx_mgr())
166+
167+
@Object
168+
def tx_mgr(self):
169+
return ConnectionFactoryTransactionManager(self.factory)
170+
171+
@Object
172+
def bank(self):
173+
return TransactionalBank(self.factory)
174+
175+
.. highlight:: xml
176+
177+
This can also be configured using :doc:`XMLConfig <objects-xmlconfig>`::
178+
179+
<?xml version="1.0" encoding="UTF-8"?>
180+
<objects xmlns="http://www.springframework.org/springpython/schema/objects/1.1"
181+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
182+
xsi:schemaLocation="http://www.springframework.org/springpython/schema/objects/1.1
183+
http://springpython.webfactional.com/schema/context/spring-python-context-1.1.xsd">
184+
185+
<object id="transactionalObject" class="springpython.database.transaction.AutoTransactionalObject">
186+
<constructor-arg ref="tx_mgr"/>
187+
</object>
188+
189+
<object id="tx_mgr" class="springpython.database.transaction.ConnectionFactoryTransactionManager">
190+
<constructor-arg ref="factory"/>
191+
</object>
192+
193+
<object id="factory" class="...your DB connection factory definition here..."/>
194+
195+
<object id="bank" class="TransactionalBank">
196+
<constructor-arg ref="factory"/>
197+
</object>
198+
199+
</objects>
200+
201+
202+
PROPAGATION_REQUIRED ...
203+
++++++++++++++++++++++++
204+
205+
Declarative transactions includes the ability to define transaction propagation.
206+
This allows you to define when a transaction should be started, and which
207+
operations need to be part of transactions. There are several levels of
208+
propagation defined:
209+
210+
* PROPAGATION_SUPPORTS - Code can run inside or outside a transaction.
211+
* PROPAGATION_REQUIRED - If there is no current transaction, one will be started.
212+
* PROPAGATION_MANDATORY - Code MUST be run inside an already started transaction.
213+
* PROPAGATION_NEVER - Code must NOT be run inside an existing transaction.
214+
215+
.. highlight:: python
216+
217+
The following code is a revision of the Bank class, with this attribute plugged in::
218+
219+
class TransactionalBankWithLotsOfTransactionalArguments(object):
220+
"""This sample application can be used to demonstrate the value of atomic operations. The transfer operation
221+
must be wrapped in a transaction in order to perform correctly. Otherwise, any errors in the deposit will
222+
allow the from-account to leak assets."""
223+
def __init__(self, factory):
224+
self.logger = logging.getLogger("springpython.test.testSupportClasses.TransactionalBankWithLotsOfTransactionalArguments")
225+
self.dt = DatabaseTemplate(factory)
226+
227+
@transactional(["PROPAGATION_REQUIRED"])
228+
def open(self, accountNum):
229+
self.logger.debug("Opening account %s with $0 balance." % accountNum)
230+
self.dt.execute("INSERT INTO account (account_num, balance) VALUES (?,?)", (accountNum, 0))
231+
232+
@transactional(["PROPAGATION_REQUIRED"])
233+
def deposit(self, amount, accountNum):
234+
self.logger.debug("Depositing $%s into %s" % (amount, accountNum))
235+
rows = self.dt.execute("UPDATE account SET balance = balance + ? WHERE account_num = ?", (amount, accountNum))
236+
if rows == 0:
237+
raise BankException("Account %s does NOT exist" % accountNum)
238+
239+
@transactional(["PROPAGATION_REQUIRED"])
240+
def withdraw(self, amount, accountNum):
241+
self.logger.debug("Withdrawing $%s from %s" % (amount, accountNum))
242+
rows = self.dt.execute("UPDATE account SET balance = balance - ? WHERE account_num = ?", (amount, accountNum))
243+
if rows == 0:
244+
raise BankException("Account %s does NOT exist" % accountNum)
245+
return amount
246+
247+
@transactional(["PROPAGATION_SUPPORTS","readOnly"])
248+
def balance(self, accountNum):
249+
self.logger.debug("Checking balance for %s" % accountNum)
250+
return self.dt.queryForObject("SELECT balance FROM account WHERE account_num = ?", (accountNum,), types.FloatType)
251+
252+
@transactional(["PROPAGATION_REQUIRED"])
253+
def transfer(self, amount, fromAccountNum, toAccountNum):
254+
self.logger.debug("Transferring $%s from %s to %s." % (amount, fromAccountNum, toAccountNum))
255+
self.withdraw(amount, fromAccountNum)
256+
self.deposit(amount, toAccountNum)
257+
258+
@transactional(["PROPAGATION_NEVER"])
259+
def nonTransactionalOperation(self):
260+
self.logger.debug("Executing non-transactional operation.")
261+
262+
@transactional(["PROPAGATION_MANDATORY"])
263+
def mandatoryOperation(self):
264+
self.logger.debug("Executing mandatory transactional operation.")
265+
266+
@transactional(["PROPAGATION_REQUIRED"])
267+
def mandatoryOperationTransactionalWrapper(self):
268+
self.mandatoryOperation()
269+
self.mandatoryOperation()
270+
271+
@transactional(["PROPAGATION_REQUIRED"])
272+
def nonTransactionalOperationTransactionalWrapper(self):
273+
self.nonTransactionalOperation()
274+
275+
You will notice several levels are being utilized. This class was pulled
276+
directly from the test suite, so some of the functions are deliberately written
277+
to generate controlled failures.
278+
279+
If you look closely at *withdraw*, *deposit*, and *transfer*, which are all set to
280+
PROPAGATION_REQUIRED, you can see what this means. If you use *withdraw* or
281+
*deposit* by themselves, which require transactions, each will start and complete
282+
a transaction. However, *transfer* works by re-using these business methods.
283+
*Transfer* itself needs to be an entire transaction, so it starts one. When it
284+
calls *withdraw* and *deposit*, those methods don't need to start another
285+
transaction because they are already inside one. In comparison, *balance* is
286+
defined as PROPAGATION_SUPPORTS. Since it doesn't update anything, it can
287+
run by itself without a transaction. However, if it is called in the middle
288+
of another transaction, it will play along.
289+
290+
You may have noticed that balance also has "readOnly" defined. In the future,
291+
this may be passed onto the RDBMS in case the relational engine can optimize
292+
the query given its read-only nature.

0 commit comments

Comments
 (0)