-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Altered PdoConnection to allow nested transaction. #5
base: master
Are you sure you want to change the base?
Conversation
Created/altered tests to account for this. Fixes phillipsdata#4
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like this for a number of reasons:
- It doesn't allow nested transactions. It simply silently ignores when
begin()
is called multiple times, then does nothing whencommit()
is called, unless you call it an equal number of times. - As mentioned above, calling
commit()
does nothing in some cases. That's unexpected behavior, and a sinkhole for bugs. - This does nothing to support real nested transactions on systems that support nested transactions, like innoDB. Nor can is solve the issue of implicit transactions (i.e. DROP TABLE) within a nested call set.
- Nested transaction calls can easily be avoided using a service layer. Controller calls Service, Service calls begin/commit/rollback around the models to perform a particular action.
/** | ||
* @var bool Flag that tells whether a rollback is needed | ||
*/ | ||
private $rollbackNeeded = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why have a flag that does that same thing as $transactionCount === 0
? Also the naming is misleading. Just because a transaction has been processed doesn't mean it needs to be rolled back.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$transactionCount represents how many transactions are currently open. $rollbackNeeded is only true when a rollback is called on a transaction within another transaction, which is very different from $transactionCount === 0. I means that if the outer transaction tries to commit, we need to rollback instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Performing a rollback within a commit is not a good design choice, imo. If you can't commit, or you can't rollback you probably need to throw an exception because that's definitely programmer error.
Also, if this is just a queue system, why not use a queue?
return $this->connect()->beginTransaction(); | ||
} | ||
|
||
return ($this->transactionCount >= 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
superfluous parens.
return $this->connect()->rollBack(); | ||
} | ||
|
||
$this->rollbackNeeded = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, flag not needed, but since it's here, kinda weird to set something to true when it must already be true to reach this line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The flag is false by default and is only set to true when a rollback is called on an inner transaction. It doesn't depend on the current value of rollbackNeeded at all.
} | ||
|
||
$this->rollbackNeeded = false; | ||
$this->connect()->rollback(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
??? Rolling back within a commit has got to be some kind of crossing-the-streams kind of bad.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, it feels a bit weird, but if one of the inner transactions fails, then we must not commit the outer transaction. We return false to indicate that this has happened.
@@ -248,7 +268,17 @@ public function rollBack() | |||
*/ | |||
public function commit() | |||
{ | |||
return $this->connect()->commit(); | |||
if (--$this->transactionCount === 0) { | |||
if (!$this->rollbackNeeded) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(╯°□°)╯︵ ┻━┻
@@ -248,7 +268,17 @@ public function rollBack() | |||
*/ | |||
public function commit() | |||
{ | |||
return $this->connect()->commit(); | |||
if (--$this->transactionCount === 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, suppose this is called before begin()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could try to make it more robust for handling cases like these, but that seems like a pretty clear dev error.
return false; | ||
} | ||
|
||
return ($this->transactionCount >= 0 && !$this->rollbackNeeded); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return
return
return
😭
imo, the only time it makes sense to return something other than false
is if you return $this->connect()->commit()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The return is meant to signal whether an error occurred. In the case were we have not reached the outer transaction, and none of the inner transactions have tried to rollback, we return true for success.
$this->connection->setConnection($this->mockConnection('beginTransaction', true)); | ||
foreach ($actions as $action) { | ||
$this->assertTrue($this->connection->begin()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
probably ought to add 'begin'
to the list of actions in the data provider.
|
||
end($actions); | ||
$end_index = key($actions); | ||
reset($actions); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(╯°□°)╯︵ ┻━┻
$end_index = count($actions) - 1;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does PSR2 allow underscore in variable names? I thought they had to be camelCase
...
} else { | ||
$this->connection->{$action}(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could be written
$end = count($actions) -1;
foreach ($actions as $index => $action) {
if ($index === $end) {
$this->connection->setConnection($this->mockConnection($action, $expected);
}
$actual = $this->connection->{$action}();
$index === $end && $this->assertEquals($expected, $actual);
}
So I could fix a lot of the things you commented on, but it looks like you disagree with this change as a concept. It did seem that you were not quite understanding the intention for some of the code though so I'll try and clear that up. You were totally correct in saying that this does not actually implement nested transactions, but is purely meant to ignore them so that one function that is currently using a transaction does not need to worry about calling another one that also uses a transaction. To that end it is designed to, as you said, only begin on the first begin, and commit on the last commit. The rollbackNeeded function will only be true when one of the "nested" transactions tries to rollback its changes, In this case, we do not want to rollback immediately, because we would then end our transaction before reaching the end of the encompassing transaction. Still, we do need to acknowledge that because the inner transaction failed, so should the outer. So in the event that the outer succeeds and tries to commit, I decided to rollback and return false based on "rollbackNeeded". In response to your conceptual objections:
|
Changed some formatting. Updated variable name. Restructured PdoConnection::commit and PdoConnectionTest::testNestedTransactions.
…s one of the actions given by the data provider and added test cases.
I presume the reason for this PR is to silently ignore when class Model {
public function saveThisThing() {
$db->begin();
// stuff
$this->saveThatThing();
$db->commit();
}
public function saveThatThing() {
$db->begin();
// stuff
$db->commit();
} To solve this on a service layer you'd simply execute a single transaction per service call. It would look something like: class ThisThingService {
public function createThisThing(Model $model) {
$db->begin();
$model->saveThisThing();
$model->saveThatThing();
$db->commit();
}
}
class Model {
public function saveThisThing() {
// stuff
}
public function saveThatThing() {
// stuff
}
} If you needed to Basically what I'm getting at is that nested transactions are a lot like buffered queries. If you need them, you're doing something wrong. |
Created/altered tests to account for this.
Fixes #4