Proposers:
Bryce Lohr
Darby Felton, Zend liaison
Overview
I propose to add a sort-of pseudo-nested transactions feature to Zend_Db_Abstract. It would just automatically merge together all the "nested" transactions within the outermost transaction, forming one large normal transaction. It would be fairly easy to implement, and would make it easier to compose together higher- and lower-level DB code that needed to rely on transactions.
| This feature would not allow rolling back or committing of nested transactions separately from the outermost transaction. Such true nested transactions are out of scope for this proposal. Needless to say, trying to implement all that in PHP would be infeasible. |
Theory of Operation
Currently, any call to beginTransaction() after the first, but before a matching call to rollback() or commit(), will result in a PDO exception. Instead, it would be trivial for beginTransaction(), commit() and rollback() to simply keep track of a counter. This counter would represent the current depth of the nested transactions. beginTransaction() would increment the counter, commit() would decrement it, and rollback() would always set it to zero. By only making a real begin or commit when the counter is set to 1, all the "nested" transactions would be transparently merged into the first, outermost transaction. However, if rollback() is called anywhere, at any nesting depth, it should immediately rollback (which would cancel the entire outermost transaction), and set the counter to zero. Thus, instead of producing an error on subsequent calls, beginTransaction() and company would simply return true, as if they were successful.
Use Cases
Composing DB code
Suppose you have a method, called singleTableSave(), that takes a table name and a data array, and stores the given data in the given table. Now, this method does a few things that you'd like to be done inside a transaction for atomicity, etc.
Example pseudo-code for singleTableSave():
Note that this code begins and ends its own transaction. Since this method knows how to save to a table, you might want call it from somewhere else; to save to several tables with foreign key relationships, for example. In this example, you'd want the entire operation to be in one transaction. But that isn't possible with the current implementation, because the call to beginTransaction() in singleTableSave() would cause an error if you have already started a transaction. Also, in this example, if some exception is thrown in singleTableSave(), you would want to roll back any outer transaction that depended on this one.
Other Use Cases
I haven't thought of any, since the previous one is what applies to me, but I'm sure there are other cases when this might be useful.
Importantly, the changes proposed here should not break any existing code, since the semantics of non-nested transactions aren't changed.
Class Skeletons
This proposal modifies existing classes, rather than adding any new ones. The patch below implements a working proof-of-concept. It could no doubt use some more robust checking for error conditions and inconsistencies.
This patch can also be found in the file attached to this wiki page.
I'm very uncomfortable with this proposal. Like it or not, transactions are "global" and they do not obey object-oriented encapsulation.
Well, except in InterBase/Firebird, which permits multiple active transactions that can be committed or rolled back independently. But this is the only RDBMS I've ever used that supported this concept. Usually a transaction is 1:1 with the database connection.
Problem scenario #1
I call commit(), are my changes committed? If I'm running inside an "inner transaction" they are not. The code that manages the outer transaction could choose to roll back, and my changes would be discarded without my knowledge or control.
Problem scenario #2
An inner transaction rolls back, it could discard legitimate changes made by an outer transaction. When control is returned to the outer code, it believes its transaction is still active and available to be committed. With your patch, they could call commit(), and side the transDepth is now 0, it would silently set transDepth to -1 and return true, after not committing anything.
This starts to sound like discussions I have had years ago with people who wanted to do transaction commit/rollback inside triggers. Yeesh! The can of worms that opens up is scary indeed.
Woops, I made this typo:
...and side the transDepth is now 0...
should be
...and since the transDepth is now 0...
Problem scenario #3
With your patch, if I call commit() when there is no transaction active, it sets the transDepth to -1. The next begin transaction increments the depth to 0, which means the transaction can neither be rolled back nor committed. Subsequent calls to commit() will just decrement the transaction to -1 or further, and you'll never be able to use a transaction again.
This is fixable – it's a coding issue instead of a conceptual issue. But I still feel that the concept is flawed. Trying to emulate transaction savepoints in application logic without allowing the database to do the bookkeeping is a doomed idea.
I acknowledge there are legitimate implementation issues with the code above, but it's about the concept, not the implementation. I suppose I was not clear enough in my intention about what this is trying to do: I in no way want to try to emulate or recreate the functionality of actual nested transactions. All those issues you mentioned are very valid and serious things you have to deal with when doing actual nested transactions, and I just want to avoid all of that outright. That's what I meant in the very first paragraph of the proposal when I said "true nested transactions are out of scope". I would not want anyone using this to go into it expecting things to work that way; they would need to know that it just merges everything into one big transaction, with only one rollback possible for the whole thing.
All I wanted to do is just automatically have transactions (that I started and ended farther down the call stack) automatically get merged together with the one I may have already started. The bugs in this should definitely be fixed; I just hacked this code straight into the Abstract adapter class to accommodate my needs. Since I was throwing exceptions whenever I needed to roll back a transaction, this suited my use case well. I just figured such functionality might be useful to other people, since it makes organizing some kinds of database access code easier (IMHO).
A while after I wrote this proposal, it occurred to me that it would be better if there was some kind of extensible transaction management mechanism I could just replace with my own code. How about separating the existing transaction methods (beginTransaction, commit, rollback) and separating them out to their own object? That way I can just set my own instance of the interface, and no one else has to deal with my bugs (conceptual or otherwise
)?
Something this:
This would be completely backwards compatible, and that way one would not have to override the abstract adapter, and all classes that inherit from it, just to modify how transactions work.
This proposal does not follow the proposal template, and we are missing the "References", "Component Requirements, Constraints, and Acceptance Criteria", "Dependencies on Other Framework Components", "Milestones / Tasks", and "Class Index" sections. We also do not have the nice header, table of contents, and other features provided by the template. I suggest copying the wiki markup from another proposal, pasting it, followed by replacing the sections' data with the content here, and then filling out the sections that are presently missing.
Way back when I first created this proposal, the new proposal template didn't work correctly, so I just created the page from scratch.
Actually, this whole idea was really just something I quickly hacked into the framework, back when it was much younger. I threw this proposal up, thinking it might be a useful feature to other people, and easy to implement. But honestly, this code has no business in the framework at all. If anything, the transaction management code should be factored out to user-replaceable objects, like I mentioned in a previous comment, but that belongs in a separate proposal (that correctly follows the template
).
I'd like to just rescind this proposal altogether.
I implemented nested transactions using two private vars (one static + one dynamic) and one private method in the db-adapter class and it does not use transaction depth. It limits the number of transactions to just one, tracks which model can commit/rollback the transaction and makes sure that only the model that began the transaction is the only model that can ever commit/rollback the transaction. This overcomes the bug pointed out in the above proposal's comments and works fine:
HTH.
Shekar
This suffers from some of the same problems I saw in the original proposal.
That is, I can call commit() or rollback() in Model B (assuming Model A has already initiated a transaction) but this request is silently a no-op. The transaction may be resolved in the opposite way later, and this happens without notifying Model B. In other words:
Model A: begin transaction
Model A: execute some changes
Model B: begin transaction (silent no-op)
Model B: execute some changes
Model B: commit (silent no-op)
Model A: rollback (discards both model A changes and model B changes)
Model B: WTF!? What happened to my changes?
If you have a requirement for two models to use explicit transaction control in one application request, then you must open two DB connections, one for each model. Then each model can have its own active transaction, which can be committed or rolled back independently from one another.
In many scenarios, I'd like to have nested transactions where either all of them succeed or all of them fail, i.e. the first failing nested transaction (whatever depth) causes a global rollback, while the last succeeding nested transaction (the outmost) causes a global commit.
I think the solution could be based on a simple template like this:
ZF Home Page
Code Browser
Wiki Dashboard
Yes, I've already implemented this in my app to support multi-table update. Having this built-in in the framework would be elegant.
Thanks!