I'm trying to show the users (in PHP) when some block of SQL actions commit failed for some reason. But it look likes PHP DBO never gets the error. This is a simplification of the actual code.
$conn = new PDO("dblib:host=$host; dbname=$db", $user, $pass);
$sql = "
BEGIN TRAN
INSERT INTO table (field) VALUES (1)
INSERT INTO table (field) VALUES (2)
INSERT INTO table (field) VALUES ('this will throw an error')
COMMIT
IF ##ERROR <> 0 BEGIN
ROLLBACK
RAISERROR('Nops.', 16, 1)
END";
try {
$rs = $conn->query($sql);
} catch (PDOException $e) {
echo 'It never gets here';
}
How do I get that this transaction was not commited?
Related
My registration form inserts a row into two tables. How can I roll back all transactions if either doesn't complete?
Here is my snippet so far:
try {
// insert row for account
$stmt = $dbh->prepare("INSERT INTO accounts (account_num) VALUES (:account)");
$params = [
":account_num" => $account_num
]
$stmt=>execute($params);
// insert row for user
$stmt = $dbh->prepare("INSERT INTO users (email, account_num) VALUES (:email, :account_num)");
$params = [
":email" => $email,
":account_num" -> $account_num;
]
$stmt->execute($params);
} catch (PDOExeception $e) {
echo "error: could not create your account and profile";
}
You can do something inside a try catch like this-
$this->pdo->beginTransaction(); //prepare database for rollback changes if needed
try{
$stmt1 = $this->pdo->prepare(...); //prepare your first statement for execution
$stmt1->execute(...); //execute first statement
$stmt2 = $this->pdo->prepare(...); //prepare your second statement for execution
$stmt2->execute(...); //execute second statement
$this->pdo->commit(); //confirms that all statements are executed and no errors occured
} catch (\PDOException $e) {
$this->pdo->rollBack(); //if there is any error, the exception handler will rollback the operation
}
Please be noted that if you have an auto incremented primary key, then you may miss two auto-incremented values here because this rollback operation first creates/ inserts the data. If any error occurred then simply deletes them. So the auto incremented primary keys might be missing.
DB Type: MariaDB
Table Engine: InnoDB
I have a table where inside it has a column with a value which is being incremented (not auto, no inserting happens in this table)
When I run the following SQL query in phpMyAdmin it works just fine as it should:
UPDATE `my_table`
SET `my_column` = LAST_INSERT_ID(`my_column` + 1)
WHERE `my_column2` = 'abc';
SELECT LAST_INSERT_ID();
The above returns me the last value for the my_column table when the query happened. This query was taken directly from the mysql docs on locking: https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html (to the bottom) and this seems to be the recommended way of working with counters when you don't want it to be affected by other connections.
My PDO:
try {
$conn = new PDO("mysql:host=$servername;dbname=$dbname", $username, $password);
// set the PDO error mode to exception
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = "UPDATE `my_table`
SET `my_column` = LAST_INSERT_ID(`my_column` + 1)
WHERE `my_column2` = 'abc';
SELECT LAST_INSERT_ID();";
// Prepare statement
$stmt = $conn->prepare($sql);
// execute the query
$stmt->execute();
$result = $stmt->fetchColumn(); // causes general error
$result = $stmt->fetch(PDO::FETCH_ASSOC);// causes general error
// echo a message to say the UPDATE succeeded
echo $stmt->rowCount() . " records UPDATED successfully";
} catch(PDOException $e) {
echo $sql . "<br>" . $e->getMessage();
}
$conn = null;
Exact error SQLSTATE[HY000]: General error, If I remove the lines where I try to get the result, it updates the column, but I still do not have a return result... how do I perform that update query and get the select result all in one go like I do when I run it in phpMyAdmin? This all needs to happen in one go as specified by the MySQL docs so I don't have issues where two connections might get the same counter.
There is no need to perform SELECT LAST_INSERT_ID();. PDO will save that value automatically for you and you can get it out of PDO.
Simply do this:
$conn = new PDO("mysql:host=$servername;dbname=$dbname;charset=utf8mb4", $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
$sql = "UPDATE `my_table`
SET `my_column` = LAST_INSERT_ID(`my_column` + 1)
WHERE `my_column2` = 'abc'";
// Prepare statement
$stmt = $conn->prepare($sql);
// execute the query
$stmt->execute();
$newID = $conn->lastInsertId();
lastInsertId() will give you the value of the argument evaluated by LAST_INSERT_ID().
Here is my script:
$id = $_GET['id'];
$value = $_GET['val'];
// database connection here
try{
$db_conn->beginTransaction();
// inserting
$stm1 = $db_conn->prepare("INSERT into table1 (col) VALUES (?)");
$stm1->execute(array($value));
// updating
$stm2 = $db_conn->prepare("UPDATE table2 SET col = "a new row inserted" WHERE id = ?");
$stm2->execute(array($id));
$db_conn->commit();
}
catch(PDOException $e){
$db_conn->rollBack();
}
All I want to know, can I use an if statement in the codes which are between beginTransaction() and commit() ? Something like this:
$id = $_GET['id'];
$value = $_GET['val'];
// database connection here
try{
$db_conn->beginTransaction();
// inserting
$stm1 = $db_conn->prepare("INSERT into table1 (col) VALUES (?)");
$stm1->execute(array($value));
// updating
if (/* a condition here */){
$stm2 = $db_conn->prepare("UPDATE table2 SET col = "a new row inserted" WHERE id = ?");
$stm2->execute(array($id));
}
$db_conn->commit();
}
catch(PDOException $e){
$db_conn->rollBack();
}
Can I ?
Actually I asked that because here is a sentence which says you can't and doing that is dangerous:
Won't work and is dangerous since you could close your transaction too early with the nested commit().
There is no problem with your transaction structure. The comment on php.net only means, that MySQL does not support nested transactions. In order to your further question, you can query any data (SQL), manipulate data (DML), but not modify any database structures (DDL - data definition language).
/*won't work*/
START TRANSACTION;
/*statement*/
START TRANSACTION; /*nested not supported, auto commit*/
/*statement*/
COMMIT;
/*statement dependend on 1st transaction won't work*/
COMMIT;
See also MySQL ref
Transactions cannot be nested. This is a consequence of the implicit commit performed for any current transaction when you issue a START TRANSACTION statement or one of its synonyms.
You can do everything within a transaction, the only thing you cannot do is nest transactions.
Not the if clause itself is the problem in your linked comment, but the fact there is another beginTransaction / commit pair inside.
Here is my script:
try{
$db_conn->beginTransaction();
$stm1 = $db_conn->prepare("UPDATE table1 SET col = 'updated' WHERE id = ?");
$stm1->execute(array($value));
$done = $stm->rowCount();
if ($done){
try {
$stm2 = $db_conn->prepare("INSERT into table2 (col) VALUES (?)");
$stm2->execute(array($id));
} catch(PDOException $e){
if ((int) $e->getCode() === 23000) { // row is duplicate
$stm3 = $db_conn->prepare("DELETE FROM table2 WHERE col = ?");
$stm3->execute(array($id));
} else {
$db_conn->rollBack(); -- this
}
}
} else {
$error = true;
}
$db_conn->commit();
}
catch(PDOException $e){
$db_conn->rollBack();
}
As you see there is a rollBack() before commit() (where I've commented by -- this). Well is what I'm doing correct? Or that rollBack() is useless?
Note: that DELETE statement acts as an undo. Suppose you give a vote to a post and you want to take it back. So that DELETE statement remove it if you send a vote twice.
You dont need to make it quite so complicated.
You can run the 2 queries within a single try/catch as any of the queries that has an isse will throw an exception, and then you can do a single rollback.
If the first query fails, the database will not be changed, the rollback will just close the transaction. If the second query fails the rollback will UNDO the first query i.e. the UPDATE you did previously.
If both queries complete OK, the commit will apply the changes to the database.
try{
$db_conn->beginTransaction();
$stm1 = $db_conn->prepare("UPDATE table1 SET col = 'updated' WHERE id = ?");
$stm1->execute(array($value));
$stm2 = $db_conn->prepare("INSERT into table2 (col) VALUES (?)");
$stm2->execute(array($id));
$db_conn->commit();
}
catch(PDOException $e){
$db_conn->rollBack();
}
ADDITIONAL NOTES
I see what you think you were trying to do. BUT!
If the INSERT fails with a 23000 error code, then the INSERT will not have been done. Your unnecessary attempt to delete the failed INSERT will actually DELETE the row that was there originally i.e. the original vote that should not be deleted!
Your code would work fine. You can get rid of your inner rollback if you would rethrow your error (that would be the "standard way"):
try {
...
if ($done){
try {
...
} catch(PDOException $e){
if ((int) $e->getCode() === 23000) { // row is duplicate
...
} else {
throw $e;
}
}
...
} else {
$error = true;
}
$db_conn->commit();
}
catch(PDOException $e){
$db_conn->rollBack();
}
In this case, your final rollback will handle all exceptions that should be rolled back, while handling the one exception 23000 yourself without rollback.
And it would work fine in your code too. Technically, you can in fact combine as many commits and rollbacks as you want without resulting in an error - if you commit after a rollback, it will just commit nothing, because it rolled back already. If you use rollback without start transaction (in autocommit mode), it will just do nothing. It is just a little harder to maintain the code and to see the strcuture, that's why you usually would use the "standard way" above.
There is just one important thing to consider: transactions in mysql are not nested. If you use start transaction, it will automatically do a commit before that. So e.g.
start transaction;
delete from very_important_table;
start transaction; -- will do an autocommit
rollback;
rollback; -- will have no effect;
commit; -- will have no effect;
rollback; -- will have no effect;
will not rollback your very_import_table, because the 2nd start transaction already committed it.
So I am trying to "move" selected rows from 1 table to another in different databases.
It in theory works (but if anyone wants to give any opinions please do, I am very new to PDO. I however keep getting a "SQLSTATE[HY000]: General error" error.
Any advice?
private function broken() {
try {
$sql = "SELECT * FROM `calls` WHERE `calls`.`status`=0 AND `calls`.`stage` < 4 AND `calls`.`answer` < (NOW() + INTERVAL 10 MINUTE)";
$query = $this->staging->query($sql);
while($row = $query->fetch(PDO::FETCH_ASSOC)) {
// Insert in production database:
$sql = "INSERT INTO `ivr_incomplete` (`id`,`sip_id`,`extension`,`caller_id`,`stage`,`status`,`survey_id`,`start`,`answer`,`hangup`,`end`) VALUES (:id, :sip_id, :extension, :caller_id, :stage, :status, :survey_id, :start, :answer, :hangup, :end)";
$query = $this->production->prepare($sql);
$query->execute($row);
// Delete from staging:
$sql = "DELETE FROM `calls` WHERE `id`='".$row['id']."'";
$this->staging->query($sql);
}
}
catch(PDOException $e) {
$this->informer("FATAL", "Unable to process broken IVR surveys. Error: ".$e->getMessage());
}
}
Two points:
You are preparing the INSERT on every iteration, which sort of eliminates half of the point of using a prepared statement - all you are using it for is escaping. One of the points of prepared statements is that the query is only parsed once, so if you need to execute the same query repeatedly with different values, calling prepare() once and then simply calling execute() with the different data sets can significantly boost performance.
This whole thing could be accomplished in 2 queries: Removed due to use of two separate DB connections
EDIT
Try this code:
You will likely need to adjust the error handling to meet your needs, particularly around how it is handled if there is an error with an INSERT, since I doubt you would want to break the whole operation and leave the rows that have been successfully processed in the source table.
private function broken() {
try {
// Fetch records to move
$sql = "
SELECT *
FROM `calls`
WHERE `status` = 0
AND `stage` < 4
AND `answer` < (NOW() + INTERVAL 10 MINUTE)
";
$query = $this->staging->query($sql);
if (!$query) {
$errorInfo = $this->staging->errorInfo();
throw new Exception("MySQL error at SELECT: $errorInfo[1] ($errorInfo[0]): $errorInfo[2]");
}
// Prepare the INSERT statement
$sql = "
INSERT INTO `ivr_incomplete`
(`id`,`sip_id`,`extension`,`caller_id`,`stage`,`status`,`survey_id`,`start`,`answer`,`hangup`,`end`)
VALUES
(:id, :sip_id, :extension, :caller_id, :stage, :status, :survey_id, :start, :answer, :hangup, :end)
";
if (!$stmt = $this->production->prepare($sql)) {
$errorInfo = $this->production->errorInfo();
throw new Exception("MySQL error at prepare INSERT: $errorInfo[1] ($errorInfo[0]): $errorInfo[2]");
}
// A list of the row IDs we are working with
$rowIds = array();
// Loop the results and insert them
for ($i = 1; $row = $query->fetch(PDO::FETCH_ASSOC); $i++) {
if (!$stmt->execute($row)) {
$errorInfo = $stmt->errorInfo();
throw new Exception("MySQL error at INSERT row $i (id: {$row['id']}): $errorInfo[1] ($errorInfo[0]): $errorInfo[2]");
}
$rowIds[] = (int) $row['id'];
}
// Delete from staging:
if ($rowIds) {
$sql = "
DELETE FROM `calls`
WHERE `id` IN (".implode(', ', $rowIds).")
";
if (!$this->staging->query($sql)) {
$errorInfo = $this->staging->errorInfo();
throw new Exception("MySQL error at DELETE: $errorInfo[1] ($errorInfo[0]): $errorInfo[2]");
}
}
} catch(PDOException $e) {
$this->informer("FATAL", "Unable to process broken IVR surveys (PDO). Error: ".$e->getMessage());
} catch (Exception $e) {
$this->informer("FATAL", "Unable to process broken IVR surveys (MySQL). Error: ".$e->getMessage());
}
}