How to lock an SQL row with PDO - php

I'm trying to lock a row of a table for updates only, while allowing reading, but the following code isn't locking the row. I executed "code1" followed by "code2" without commiting code1, through a POST request. So each of those codes lies in a distinct .php file.
I also tried it with "SELECT ... FOR UPDATE", but that didn't work too.
code1:
try{
$pdo->beginTransaction();
$sql = "SELECT assertedFlags FROM statementcontent WHERE id = $statementId LOCK IN SHARE MODE;";
$statement = $pdo->query($sql);
var_dump($statement->fetchAll());
}catch(Exception $e){
$pdo->rollBack();
exit('Exception ' . $e->getMessage());
}
code2:
try{
$pdo->beginTransaction();
$sql = "SELECT assertedFlags FROM statementcontent WHERE id = $statementId LOCK IN SHARE MODE;";
$statement = $pdo->query($sql);
var_dump($statement->fetchAll());
//New segment starts here:
$sql = "UPDATE statementcontent SET assertedFlags = '4' WHERE id = $statementId;";
$statement = $pdo->exec($sql);
var_dump($statement);
$pdo->commit();
}catch(Exception $e){
$pdo->rollBack();
exit('Exception ' . $e->getMessage());
}
Could it be that it's not locked because after code1 exits the lock gets released?
EDIT
I put both in one file with two pdo instances and this time the lock worked. I don't really understand how a server manages queries when having many requests. Are there going to be many processes or many threads? Will it stay locked for all cases or only in the case of many threads?
Thanks.

For proper row locking you should look at:
https://dev.mysql.com/doc/refman/5.5/en/innodb-locking.html
However this requires your database table is innodb not myisam.

Related

PHP mysql write lock doesn't allow updating database. Connection which locks should be able to update

I'm trying to lock (WRITE) a table in database, and while it's locked I want to update some field and then unlock table. It seems that once I lock a table, the connection which locked table in also locked out. But the point of WRITE lock is to disable other connections, but not the one doing the locking. I tried it using mysqli and PDO, but none of it seems to work.
I also tried directly in phpmyadmin, and it works fine.
I'm using PHP 5.6 and mysqli/PDO extensions.
If I execute it directly in phpmyadmin than it works:
LOCK TABLE `catalogue_user` WRITE;
UPDATE `catalogue_user` SET update_time = NOW() WHERE catalogue_user_id = 5;
SELECT SLEEP(10);
SELECT NOW();
I need "update_time" to be updated immediately after locking (while others can't access table)
try {
$conn = new PDO("mysql:host=". DB_HOST .";dbname=". DB_DBNAME, DB_USER, DB_PASS);
// set the PDO error mode to exception
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
echo "Connected successfully";
var_dump(date("H:i:s"));
$conn->exec("LOCK TABLE ". DB_TABLE_CATALOGUE_USER ." WRITE;");
$conn->exec("UPDATE ". DB_TABLE_CATALOGUE_USER ." SET update_time = '". date("H:i:s") ."' WHERE catalogue_user_id = 5");
sleep(10);
$conn->exec("UNLOCK TABLES;");
var_dump(date("H:i:s"));
}
catch(PDOException $e)
{
echo "Connection failed: " . $e->getMessage();
}
I've searched allover for possible clues, but none of them worked...

PHP, MySQL, PDO Transactions - Can fetchAll() come before commit()?

More transaction questions!
What I have right now is a mess of strung-together queries, that are all manually reversed if any fail:
Code Block 1
$stmt1 = $db->prepare(...); // Update table1, set col=col+1
if($stmt1 = $db->execute(...)){
$stmt2 = $db->prepare(...); // Insert into table2, id=12345
if($stmt2 = $db->execute(...)){
$stmt3 = $db->prepare(...); // Select val from table3
if($stmt3 = $db->execute(...)){
$result = $stmt3->fetchAll();
if($result[0]['val'] == something){
$stmt4 = $db->prepare(...); // Update table4, set status=2
if($stmt4 = $db->execute(...)){
return true;
}else{
$stmt1 = $db->prepare(...); // Update table1, set col=col-1 (opposite of above)
$stmt1 = $db->execute(...);
$stmt2 = $db->prepare(...); // Delete from table2, where id=12345 (opposite of above)
$stmt2 = $db->execute(...);
return false;
}
}
return true;
}else{
$stmt1 = $db->prepare(...); // Update table1, set col=col-1 (opposite of above)
$stmt1 = $db->execute(...);
$stmt2 = $db->prepare(...); // Delete from table2, where id=12345 (opposite of above)
$stmt2 = $db->execute(...);
return false;
}
}else{
$stmt1 = $db->prepare(...); // Update table1, set col=col-1 (opposite of above)
$stmt1 = $db->execute(...);
return false;
}
}
It's a mess, difficult to debug, difficult to add on to, difficult to understand when the queries are large, and won't return all tables back to original state if the connection is lost mid-way through.
This same process is even worse when deleting a row, because everything in it needs to be stored - just in case the operation needs to be undone.
Now, I know most of this will still work when I port it over to a single transaction, but the one part I am unsure of is:
Code Block 2
$result = $stmt3->fetchAll();
if($result[0]['val'] == something){
... continue ...
}else{
... reverse operations ...
return false;
}
Because the results-gathering would take place before the commit() in the transaction. As follows:
Code Block 3
$db->beginTransaction();
try{
$stmt1 = $db->prepare(...);
$stmt1->execute();
$stmt2 = $db->prepare(...);
$stmt2->execute();
$stmt3 = $db->prepare(...);
$stmt3->execute();
$result = $stmt3->fetchAll();
if($result[0]['val'] == something){
$stmt4 = $db->prepare(...);
$stmt4->execute();
}else{
$db->rollBack();
return false;
}
$db->commit();
return true;
}catch(Exception $e){
$db->rollBack();
throw $e;
return false;
}
Will this work? Specifically, can I include the $result = $stmt3->fetchAll(); before the commit(), and then execute the conditional query?
Also, I'm not entirely sure on this, but do I require the $db->rollBack(); within the try block, if the code is exited (return false) before the commit()?
Your first question:
Specifically, can I include the $result = $stmt3->fetchAll(); before the commit(), and then execute the conditional query?
I see no reason why it should not work. A transaction behaves basically the same as operations without transactions - except that changes are only drafts. Any changes you make in the previous statements will be applied to a "working copy" valid for this single session only. For you it will appear completely transparent. However any changes will be rolled back if you do not commit them.
Also worth noting (emphasis mine):
In layman's terms, any work carried out in a transaction, even if it is carried out in stages, is guaranteed to be applied to the database safely, and without interference from other connections, when it is committed.
This can cause racing conditions.
Your second question:
Also, I'm not entirely sure on this, but do I require the $db->rollBack(); within the try block, if the code is exited (return false) before the commit()?
From the documentation it says:
When the script ends or when a connection is about to be closed, if you have an outstanding transaction, PDO will automatically roll it back.
Therefore you do not necessarily require to roll back manually as it will be done by the driver itself.
However note the following from the same source as well:
Warning PDO only checks for transaction capabilities on driver level. If certain runtime conditions mean that transactions are unavailable, PDO::beginTransaction() will still return TRUE without error if the database server accepts the request to start a transaction.
So be sure to check the compatibility beforehand!
A few notes
Do NOT begin a transaction in another transaction. This will commit the first transaction implicitely. See this comment.
Another note from the documentation:
Some databases, including MySQL, automatically issue an implicit COMMIT when a database definition language (DDL) statement such as DROP TABLE or CREATE TABLE is issued within a transaction. The implicit COMMIT will prevent you from rolling back any other changes within the transaction boundary.

How can I SELECT rows from a temporary table using PDO or MySqli?

The following query works in phpMyAdmin but NOT from the php script neither when I use mysql or pdo. Normal select queries do work from the same php script.
CREATE TEMPORARY TABLE tmptable1 (INDEX myindex (sid)) SELECT * FROM `table1` WHERE uid = 55 ORDER BY cc;
SELECT * FROM tmptable1 GROUP BY sid;
I have PHP Version 5.5.0 and mysqlnd 5.0.11-dev on a LAMP server.
I read on stackoverflow that you can't use pdo for multiple queries. However in this question two methods were described which both didn't work for me.
I tried using this setting, but it doesn't work:
$db = new PDO("mysql:host=localhost;dbname=test", 'root', '');
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, 0);
$sql = "
DELETE FROM car;
INSERT INTO car(name, type) VALUES ('car1', 'coupe');
INSERT INTO car(name, type) VALUES ('car2', 'coupe');
";
try {
$db->exec($sql);
}
catch (PDOException $e)
{
echo $e->getMessage();
die();
}
I tried another approach suggested by another answer on stackoverflow, but it said that the function is unknown:
$db = new PDO("mysql:host=localhost;dbname=test", 'root', '');
// works not with the following set to 0. You can comment this line as 1 is default
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, 1);
$sql = "
DELETE FROM car;
INSERT INTO car(name, type) VALUES ('car1', 'coupe');
INSERT INTO car(name, type) VALUES ('car2', 'coupe');
";
try {
$stmt = $db->prepare($sql);
$stmt->execute();
}
catch (PDOException $e)
{
echo $e->getMessage();
die();
}
How can I get this query to fetch results from within my php script?
Execute both queries one by one.
$db->query($query1); // Create temporary table
$db->query($query2); // Fetch from it
There is no reason why that should not work.
Temporary tables are available only in the same connection. Once the connection is destroyed/closed, so are the temporary tables created during this connection.
In your script I don't see the creation of tmp table, does it means you create it in another script and therefore connection ?
And yes, I would recommend you to do three separate queries. Even if you find a way to do all three in one. It is very unlikely that this cause a performance issue, but you'll gain code clarity for sure.

Is there any way to make this database lock more efficient?

I've written this code to ensure that two users can't edit the same row of the same table at the same time. The only problem that I have with it, is that it connects to the database 3 times, once to add a new lock, once to check that it's the only lock for that row and once to either delete the lock or to retrieve the data for the user to edit. I really don't like this, but it was the only way that I could imagine doing it.
Are there any ways of making this more efficient?
<?php
$CountryID = $_GET["CountryID"];
$userID = $_SESSION["userID"];
$currentTime = date('Y-m-d H:i:s');
try{
include_once 'PDO.php';
//Adds a new lock into the database.
$dbh->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
$sth = $dbh->prepare('INSERT INTO lock (UserID, TableID, RowID) VALUES (?, ?, ?)');
$sth->execute(array($userID,1,$CountryID));
$insertedID = $dbh->lastInsertId();
//Checks to see if there is more than one lock for this table/row. If there is more than 1 row, it will check to make sure that all OTHER rows are older than 5 minutes old incase of broken locks.
$sth = $dbh->prepare('SELECT * from lock where TableID = ? AND RowID = ?');
$sth->execute(array(1,$CountryID));
$rowCount = $sth ->rowCount();
$locked = false;
if ($rowCount >1 ){
foreach($sth as $row){
if ($row['LockID'] != $insertedID AND (abs(strtotime($currentTime) - strtotime($row['Date']))) < 300){
$locked = true;
break;
}
}
}
if ($locked){
//Delete the lock we put in first, and tell the user that someone is already editing that field.
$sth = $dbh->prepare('DELETE from lock where LockID = ?');
$sth->execute(array($insertedID));
echo "Row is currently being edited.";
}else{
//Don't delete the lock, and get data from the country table.
echo "Row isn't being edited.";
$sth = $dbh->prepare('SELECT * from country where CountryID = ?');
$sth->execute(array($CountryID));
}
}catch (PDOException $e){
echo "Something went wrong: " . $e->getMessage();
}
$dbh = null;
?>
It looks like you want a scheme for advisory persistent locking. That is, you want to keep the locks in place for a relatively long time -- longer than a DBMS transaction could reasonably expect to do from a web app. I call it "advisory" because it's not "mandatory" in the sense that the DBMS enforces it.
You are very close. I suggest you define your lock table with a compound primary key (TableID, RowID). That way, attempts to insert duplicate records into that table will fail. Forget about the LockID. You don't need it. UserID is helpful because it will give you a hint at diagnosing trouble.
Then, to set up a lock (in your example on table 1, row $CountryID) you will do your insert like so:
$dbh->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); /* only once*/
$sth = $dbh->prepare('INSERT INTO lock (UserID, TableID, RowID) VALUES (?, ?, ?)');
$locked = true;
try {
$sth->execute(array($userID,1,$CountryID));
}
catch (PDOException $e) {
$locked = false;
}
This is nice because you'll handle the duplicate-key error (integrity constraint violation) by setting $locked to false. When $locked is false you know somebody else has the lock and you can't have it. It's also nice because it's race-condition-proof. If two users are racing to take the same lock, one of them definitively wins and the other definitively loses.
When you want to release the lock, do the delete in a similar fashion.
$sth = $dbh->prepare('DELETE FROM lock WHERE TableID = ? AND RowID = ?');
$it_was_locked = false;
$sth->execute(array(1,$CountryID));
if ($sth->rowCount() > 0) {
$it_was_locked = true;
}
Here, the variable $it_was_locked lets you know whether the lock was already in place. In any case after you run this command the lock will be cleared.
One more thing. For the integrity of your system, please consider defining a lock timeout. Perhaps it should be ten seconds, and perhaps it should be ten hours: that's up to your application's user-experience needs. The timeout will keep your application from getting all jammed up if people start but don't complete transactions.
Then, put a locktime column into your lock table and automatically put the current time into it. You'd do this with a line like this in your table definition.
locktime TIMESTAMP DEFAULT CURRENT_TIMESTAMP
Then, whenever you insert a lock, first release all locks that have timed out, like so.
$stclean = $dbh->prepare('DELETE FROM lock WHERE locktime < NOW() - 10 SECOND');
$sth = $dbh->prepare('INSERT INTO lock (UserID, TableID, RowID) VALUES (?, ?, ?)');
$locked = true;
$stclean->execute();
if ($stclean->rowCount() > 0) {
/* some timed-out locks were released; make an error log entry or whatever */
}
try {
$sth->execute(array($userID,1,$CountryID));
}
catch (PDOException $e) {
$locked = false;
}
This kind of thing makes for a serviceable locking scheme. When you're doing system test, you can expect to look at this lock table a lot, trying to figure out what module forget to take a lock and what module forgot to release it.

Why does my PHP transaction not work?

I'm working on a school project creating a CMS for my portfolio site. I am having trouble getting my update function to work. I have a feeling it has something to do with the way I'm constructing my PDO Transaction. In my database I have a projects table, category table, and the associative content_category table. I'm able to insert my projects into those tables just fine. What I want to do is insert into my projects table then delete all records from the content_category table and finally insert the current category records into that associative table to complete the transaction. I do get my return statement "Project Updated" returned. But the tables aren't being updated. Any ideas anyone?
Here's the code:
This is a function in my Project class.
public function update(){
try {
$conn = getConnection();
$conn->beginTransaction();
$sql = "UPDATE project
SET project_title = :title,
project_description = :desc,
project_isFeatured = :feat,
project_mainImage = :image
WHERE project_id = :id";
$st = $conn->prepare($sql);
$st->bindValue(":id", $this->id, PDO::PARAM_INT);
$st->bindValue(":title", $this->title, PDO::PARAM_STR);
$st->bindValue(":desc", $this->description, PDO::PARAM_STR);
$st->bindValue(":feat", $this->isFeatured, PDO::PARAM_BOOL);
$st->bindValue(":image", $this->mainImage, PDO::PARAM_INT);
$st->execute();
$sql = "DELETE from content_category
WHERE content_id = :id";
$st = $conn->prepare($sql);
$st->bindValue("id", $this->id, PDO::PARAM_INT);
$st->execute();
$sql = "INSERT into content_category (content_id, cat_id)
VALUES (?,?)";
$st = $conn->prepare($sql);
foreach($this->categories as $key=>$value){
$st->execute(array(intval($projectID), intval($value)));
}
$conn->commit();
$conn = null;
return "Project updated";
}
catch(Exception $e) {
echo $e->getMessage();
$conn->rollBack();
return "Error... Unable to update!";
}
}
Your database engine for the tables needs to be INNODB. If you are using phpMyAdmin, it defaults to MyISAM. (I don't know if that will cause the updates not to go through or just the transaction line to be ignored. Edit: Pretty sure the documentation is saying that it will throw an error and not do anything if you beginTransaction on a myISAM)
In order to make sure you are not encountering a PDO error, you should set the PDO error reporting like this:
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
There are functions in PDO such as prepare() which will either return false or throw a PDOException depending on what error mode is set. This way, it will throw an exception and you'll definitely know if you are having a problem!
Also, if your database doesn't support transactions (like MyISAM), the beginTransaction() function will return false. So, maybe add a check in there like:
if($conn->beginTransaction()) {
// Do transaction here
} else {
echo("Unable to use transactions with this database.");
}
Oddly enough, according to PHP documentation, you would be getting an exception if your database doesn't support transactions...
Unfortunately, not every database supports transactions, so PDO needs to run in what is known as "auto-commit" mode when you first open the connection. Auto-commit mode means that every query that you run has its own implicit transaction, if the database supports it, or no transaction if the database doesn't support transactions. If you need a transaction, you must use the PDO::beginTransaction() method to initiate one. If the underlying driver does not support transactions, a PDOException will be thrown (regardless of your error handling settings: this is always a serious error condition).
Commit returns TRUE on success or FALSE on failure. You can check it. Also check for errorCode.

Categories