|
1 | 1 | 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