Hack 67. Lock Implicitly Within Transactions
By following some simple rules, you can write queries that avoid the risk of concurrency problems without worrying about locking. You can avoid having to set locks explicitly by performing several changes in a single UPDATE statement. With AUTOCOMMIT and single-statement SQL transactions, you can forget about locking and COMMIT / ROLLBACK .
Consider Hapless Bank, which holds its customers'
Table 9-2. Bank balances
Suppose you need to transfer $3 from Ritchie's account to Archie's account. You can perform the transfer only if both accounts exist and Ritchie has at least $3 in his account.
You can update both balances and implement the condition in a single
UPDATE
statement. As every SQL statement is
UPDATE bank
SET balance = CASE WHEN name='Archie' THEN balance+3
WHEN name='Ritchie' THEN balance-3
END
WHERE name IN ('Archie','Ritchie')
AND EXISTS (SELECT name FROM bank WHERE name='Archie')
AND EXISTS (SELECT name FROM bank WHERE name='Ritchie' AND balance>=3);
This works well for SQL Server, Oracle, and PostgreSQL. In MySQL, you cannot include the table being updated in the WHERE clause of an UPDATE statement. However, MySQL does permit you to update a JOIN , so you can exploit this by performing the tests in a derived table:
UPDATE bank w CROSS JOIN (SELECT COUNT(*) AS c FROM bank
WHERE name='Archie'
OR (name='Ritchie' AND balance>=3)) t
SET w.balance = CASE WHEN w.name='Archie' THEN w.balance+3
WHEN w.name='Ritchie' THEN w.balance-3
END
WHERE w.name IN ('Archie','Ritchie')
AND t.c=2;
|
Hack 68. Cope with Unexpected Redo
When a
Suppose you are writing user account creation routines for a web application. A user can create an account on your system, where the username is his preferred email address. As part of the process, the user will be given a random password if he was successful, or will be shown an error message if appropriate. The form uses USER for username and PASS for password. The username column is the primary key of usertable : CREATE TABLE usertable ( username varchar(20) primary key, password varchar(20) ); Here is the PHP to provide a user creation form:
<?
function randomPass ( ) {
srand((double)microtime( )*1000000);
return rand(0,10000);
}
mysql_connect('localhost','
username
','
password
') or die(mysql_error( ));
mysql_select_db('
dbname
') or die(mysql_error( ));
if (! array_key_exists('_submit_check', $_POST)) {
print '<form method="POST">';
print '<input type="text" name="username" value="">';
print '<input type="submit" name="Order">';
print '<input type="hidden" name="_submit_check" value="1"/>';
print '</form>';
} else {
$sql = sprintf("SELECT username FROM usertable WHERE username = '%s%%'",
mysql_real_escape_string($_POST['username']));
$cursor = mysql_query($sql) or die(mysql_error( ));
$found = 0;
while ($line = mysql_fetch_array($cursor,MYSQL_ASSOC)){
$found = 1;
}
if ($found == 1) {
print '<p>That username is already in use</p>';
} else {
$pass = randomPass( );
$sql = sprintf("INSERT INTO usertable VALUES('%s%%','%s')",
mysql_real_escape_string($_POST['username']),$pass);
$cursor = mysql_query($sql) or die(mysql_error( ));
print '<p>Your account has been created, password '.$pass.'</p>';
}
}
?>
This user creation script is nonrepeatable. This means that running the code with the same input twice results in two different messages. Often this type of approach is accompanied by a warning that appears
If the user
does
click the button twice, he receives an error message
One way to make the query repeatable is to put a hidden field, random , in your form. When the form is generated set the hidden field to a random number. Now change the code so that multiple submissions with that value for random involving the same username always appear to succeed. You need to store the random value in usertable , in this case in a column called ccode : ALTER TABLE usertable ADD ccode INT; The new PHP code looks like this:
<?
function randomPass ( ) {
srand((double)microtime( )*1000000);
return rand(0,10000);
}
function randomCode ( ) {
srand((double)microtime( )*1000000);
return rand(0,10000000);
}
mysql_connect('localhost','
user
','
password
') or die(mysql_error( ));
mysql_select_db('
dbname
') or die(mysql_error( ));
if (! array_key_exists('_submit_check', $_POST)) {
print '<form method="POST">';
print '<input type="text" name="username" value="">';
print '<input type="submit" name="Order">';
print '<input type="hidden" name="_submit_check" value="1"/>';
print '<input type="hidden" name="random" value="'.randomCode( ).'"/>';
print '</form>';
} else {
$sql = sprintf("SELECT username,password,ccode FROM usertable
WHERE username = '%s'",
mysql_real_escape_string($_POST['username']));
$cursor = mysql_query($sql) or die(mysql_error( ));
$found = 0;
$pass = randomPass( );
while ($line = mysql_fetch_array($cursor,MYSQL_ASSOC)){
$found = 1;
if ($line{ccode} == $_POST['random']) {
$found = 2;
$pass=$line{password};
}
}
if ($found == 1) {
print '<p>That username is already in use</p>';
} elseif ($found == 2) {
print '<p>Your pressed the submit button more than once. But thats ok!</p>';
print '<p>Your account has been created, password '.$pass.'</p>';
} elseif ($found == 0) {
if (!preg_match("/^\d+$/", $_POST['random'])){
print "SQL Injection Attack Alert.";
exit(1);
}
$sql = sprintf("INSERT INTO usertable VALUES('%s','%s',%d)",
mysql_real_escape_string($_POST['username']),$pass
,$_POST['random']);
$cursor = mysql_query($sql) or die(mysql_error( ));
print '<p>Your account has been created, password '.$pass.'</p>';
}
}
?>
The new code allows you to click the Submit Query button as many times as you want, and the system will always seem to work right. Figure 9-2, Figure 9-3, and Figure 9-4 show examples of it in operation. Figure 9-2. Registering using your email address
Figure 9-3. The first time Submit Query is clicked
Figure 9-4. Second and
|