Multiple CREATE TABLE with errors within one transaction - no PDO exception - php

I'm trying to create four tables within one PDO transaction.
When the first "CREATE TABLE ..." part of the statement contains errors I successfully get an exception, an error message, and rollback. But when the first "CREATE TABLE ..." part are written correctly (as in the example below) I get no exception, commit, and only the first table created.
Here's the code:
$conn = Connection::getInstance();
$conn->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$conn->dbh->beginTransaction();
$stmt = $conn->dbh->prepare("
CREATE TABLE IF NOT EXISTS `1st table` (valid SQL-code)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
CREATE TABLE IF NOT EXISTS `2nd table` (SQL-code with an error)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
CREATE TABLE IF NOT EXISTS `3rd table`...
CREATE TABLE IF NOT EXISTS `4th table`...
");
try
{
$stmt->execute();
$conn->dbh->commit();
}
catch (Exception $e)
{
$conn->dbh->rollBack();
echo $e->getMessage();
}
unset($stmt);
After some research I found the following note at php.net:
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.
Is it that what causes the problem and how to solve it?

Like several commenters suggested, transactional semantics are not available for data definition language statements (DDL) in mySQL.
The PDO implementers obviously did not attempt to add some kind of client-side transactional semantics where there is no support in the DBMS.

Related

PHP mysqli is NOT trapping some errors when calling stored procedure

We're building a production PHP-MySQL application, and want MySQL stored procedures to be the central bullet-proof gateway to the database. Duplicate keys, table not found, server instance going down, etc all and any kind of error needs to be trapped and conveyed to the calling PHP web-based UI, and transaction rolled back in the stored proc upon such errors.
I am using PHP mysqli and calling a stored procedure as follows:
$stmt = mysqli_prepare($db, "call my_stored_proc(?, ?, ?, #ptid)");
if ($stmt && mysqli_stmt_bind_param($stmt, "sss", 'p1', 'p2', 'p3') &&
mysqli_stmt_execute($stmt) && mysqli_stmt_close($stmt)) {
echo "All fine!"
} else {
echo mysqli_error($db);
db_disconnect($db);
exit;
}
The stored procedure does some basic validation and signals a user-defined condition if the validation fails. And sure enough, my PHP code is able to catch and see those non-database (eg. formatting) validation errors arising from the stored procedure. After the non-database validations pass, the stored procedure goes on to do a database-related validation and if those pass it inserts a row in a table, and passes the ID in the last OUT parameter.
My problem is that if this insert fails (say, bcoz duplicate key error, or table not found error), my PHP code is simply not catching the error! It prints "All fine"!
Why is that? What am I missing?
I want my invocation of the stored proc to be bullet-proof, and all errors raised by the stored proc should be trappable in PHP.
FYI: If I call the stored proc from a mysql client (like MySQL Workbench or the mysql client on Linux), the errors are correctly reported.
LATER EDITS:
FYI, the stored procedure code is simply:
delimiter $$
drop procedure if exists my_stored_proc $$
create procedure my_stored_proc
(
in p_name VARCHAR(31),
in p_notes VARCHAR(510),
in p_created_by VARCHAR(31),
out p_pt_id INT
)
begin
declare custom_exception condition for sqlstate '45000';
declare l_retval boolean;
declare l_right_now datetime default now();
select p_name regexp '^[[:space:]]*$' into l_retval;
if l_retval then
signal custom_exception set message_text = 'NAME cannot be blank.';
end if;
select p_name regexp '[^0-9_]' into l_retval;
if l_retval then
signal custom_exception set message_text = 'Invalid NAME.';
end if;
call validate_user_in_db(p_created_by, true, l_retval);
if not l_retval then
signal custom_exception set message_text = 'Invalid CREATED_BY user.';
end if;
insert into some_table
(
NAME, NOTES,
CREATED_BY, CREATED_ON
) values
(
p_name, p_notes,
p_created_by, l_right_now
);
set p_pt_id = last_insert_id();
end $$
delimiter ;
EVEN LATER UPDATE:
The weird thing is, if I comment out the call to validate_user_in_db in the above stored proc, things work fine and errors are correctly trapped (eg. duplicate key, etc) in PHP.
FYI: validate_user_in_db does the following:
create procedure validate_user_in_db (in p_user VARCHAR(127),
in p_active_only boolean, out p_retval boolean)
begin
set p_retval = false;
if p_active_only then
select sql_calc_found_rows 'x'
from SOME_USERS_TABLE
where username = p_user
and active = true
limit 1;
else
select sql_calc_found_rows 'x'
from SOME_USERS_TABLE
where username = p_user
limit 1;
end if;
set #l_num_rows = found_rows() ;
if #l_num_rows = 1 then
set p_retval = true;
end if;
end $$
Sorry for the long post. But I thought I'd give the full picture.
What am I missing? Why is my PHP code not getting back errors if the call to validate_user_in_db is enabled? Is validate_user_in_db changing some state permanently? Is the sql_calc_found_rows keyword messing things up?
FYI: This is PHP 7.3 and MySQL 5.6
Aah, after breaking my head against it for long and a lot googling, I found the problem! It is closely related to How to call a stored procedure within another stored procedure (PHP and mysqli)
Basically I had a case of PHP calling SP1, which in turn called SP2, and everything working fine in a mysql client but breaking when called by PHP!
It turns out the problem is that SP2 was SELECTing a result set (ie. SELECT without an INTO clause).
I re-wrote SP2 to necessarily do a SELECT INTO and that fixed the problem.
I think the ability to SELECT a result set without doing a SELECT INTO is a crappy feature in MySQL. Come to think of it, quite a few things crappy about MySQL (stored functions, exception propagation up the stack, poor compilation and syntax error pinpointing in stored procedures, bad concept of transaction boundaries, etc).
I think SELECTing a result set in a stored routine should be avoided at all costs.
PHP reports errors from Stored Procedures. The problem here is that calling Stored Procedures through mysqli is not an easy task. It's best to avoid Stored Procedures and mysqli if you can.
All errors can be reported by mysqli if you enable mysqli error reporting. Simply add this line before opening a new connection:
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
Note: until recently mysqli had plenty of bugs that either crashed PHP or didn't report errors properly. Keep your PHP up-to-date to avoid such problems.
Your main problem is that your stored procedure is producing results, which you are not reading in PHP. Results from MySQL are fetched sequentially, even if the queries are executed asynchronously. When an error happens after SELECT in your stored procedure then PHP will not throw the error immediately. You must fetch each result, even if you don't need them in PHP.
You can utilize a simple do-while loop to fetch all results.
$stmt = $mysqli->prepare('CALL SP1()');
echo $stmt->execute();
do {
$stmt->get_result();
} while ($y = $stmt->next_result());

Getting "active unbuffered queries" PDO error in Laravel transaction tests [duplicate]

My server runs CentOS 6.4 with MySQL 5.1.69 installed using yum with CentOS's repos, and PHP 5.4.16 installed using yum with ius's repos. Edit3 Upgraded to MySQL Server version: 5.5.31 Distributed by The IUS Community Project, and error still exists. Then changed library to mysqlnd, and seems to eliminate the error. Still, with this back and forth, need to know why this error only sometimes manifests.
When using PDO and creating the PDO object using PDO::ATTR_EMULATE_PREPARES=>false, I sometimes get the following error:
Table Name - zipcodes
Error in query:
SELECT id FROM cities WHERE name=? AND states_id=?
SQLSTATE[HY000]: General error: 2014 Cannot execute queries while other unbuffered queries are active. Consider using PDOStatement::fetchAll(). Alternatively, if your code is only ever going to run against mysql, you may enable query buffering by setting the PDO::MYSQL_ATTR_USE_BUFFERED_QUERY attribute.
File Name: /var/www/initial_install/build_database.php
Line: 547
Time of Error: Tuesday July 2, 2013, 5:52:48 PDT
Line 547 is the last line of:
$stmt_check_county->execute(array($data[5],$data[4]));
if(!$county_id=$stmt_check_county->fetchColumn())
{
$stmt_counties->execute(array($data[5]));
$county_id=db::db()->lastInsertId();
}
//$stmt_check_county->closeCursor(); //This will fix the error
$stmt_check_city->execute(array($data[3],$data[4]));
I had a similar problem several years ago, but upgraded from PHP 5.1 to PHP 5.3 (and MySQL probably was updated as well), and the problem magically went away, and now I have it with PHP 5.5.
Why does it only manifest itself when PDO::ATTR_EMULATE_PREPARES=>false, and with only alternating version of PHPs?
I've also found that closeCursor() will also fix the error. Should this always be done after every SELECT query where fetchAll() is not used? Note that the error still occurs even if the query is something like SELECT COUNT(col2) which only returns one value.
Edit By the way, this is how I create my connection. I've only recently added MYSQL_ATTR_USE_BUFFERED_QUERY=>true, however, it doesn't cure the error. Also, the following script could be used as is to create the error.
function sql_error($e,$sql=NULL){return('<h1>Error in query:</h1><p>'.$sql.'</p><p>'.$e->getMessage().'</p><p>File Name: '.$e->getFile().' Line: '.$e->getLine().'</p>');}
class db {
private static $instance = NULL;
private function __construct() {} //Make private
private function __clone(){} //Make private
public static function db() //Get instance of DB
{
if (!self::$instance)
{
//try{self::$instance = new PDO("mysql:host=localhost;dbname=myDB;charset=utf8",'myUsername','myPassword',array(PDO::ATTR_EMULATE_PREPARES=>false,PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC));}
try{self::$instance = new PDO("mysql:host=localhost;dbname=myDB;charset=utf8",'myUsername','myPassword',array(PDO::ATTR_EMULATE_PREPARES=>false,PDO::MYSQL_ATTR_USE_BUFFERED_QUERY=>true,PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC));}
//try{self::$instance = new PDO("mysql:host=localhost;dbname=myDB;charset=utf8",'myUsername','myPassword',array(PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC));}
catch(PDOException $e){echo(sql_error($e));}
}
return self::$instance;
}
}
$row=array(
'zipcodes_id'=>'55555',
'cities_id'=>123
);
$data=array($row,$row,$row,$row);
$sql = 'CREATE TEMPORARY TABLE temp1(temp_id INT UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY (temp_id) )';
db::db()->exec($sql);
$sql='SELECT COUNT(*) AS valid FROM cities_has_zipcodes WHERE cities_id=? AND zipcodes_id=?';
$stmt1 = db::db()->prepare($sql);
$sql ='SELECT temp_id FROM temp1';
$stmt2 = db::db()->prepare($sql);
foreach($data AS $row)
{
try
{
$stmt1->execute(array($row['zipcodes_id'],$row['cities_id']));
$rs1 = $stmt1->fetch(PDO::FETCH_ASSOC);
//$stmt1->closeCursor();
syslog(LOG_INFO,'$rs1: '.print_r($rs1,1).' '.rand());
$stmt2->execute();
$rs2 = $stmt2->fetch(PDO::FETCH_ASSOC);
syslog(LOG_INFO,'$rs2: '.print_r($rs2,1).' '.rand());
}
catch(PDOException $e){echo(sql_error($e));}
}
echo('done');
The MySQL client protocol doesn't allow more than one query to be "in progress." That is, you've executed a query and you've fetched some of the results, but not all -- then you try to execute a second query. If the first query still has rows to return, the second query gets an error.
Client libraries get around this by fetching all the rows of the first query implicitly upon first fetch, and then subsequent fetches simply iterate over the internally cached results. This gives them the opportunity to close the cursor (as far as the MySQL server is concerned). This is the "buffered query." This works the same as using fetchAll(), in that both cases must allocate enough memory in the PHP client to hold the full result set.
The difference is that a buffered query holds the result in the MySQL client library, so PHP can't access the rows until you fetch() each row sequentially. Whereas fetchAll() immediately populates a PHP array for all the results, allowing you access any random row.
The chief reason not to use fetchAll() is that a result might be too large to fit in your PHP memory_limit. But it appears your query results have just one row anyway, so that shouldn't be a problem.
You can closeCursor() to "abandon" a result before you've fetched the last row. The MySQL server gets notified that it can discard that result on the server side, and then you can execute another query. You shouldn't closeCursor() until you're done fetching a given result set.
Also: I notice you're executing your $stmt2 over and over inside the loop, but it will return the same result each time. On the principle of moving loop-invariant code out of the loop, you should have executed this once before starting the loop, and saved the result in a PHP variable. So regardless of using buffered queries or fetchAll(), there's no need for you to nest your queries.
So I would recommend writing your code this way:
$sql ='SELECT temp_id FROM temp1';
$stmt2 = db::db()->prepare($sql);
$stmt2->execute();
$rs2 = $stmt2->fetchAll(PDO::FETCH_ASSOC);
$stmt2->closeCursor();
$sql='SELECT COUNT(*) AS valid FROM cities_has_zipcodes
WHERE cities_id=:cities_id AND zipcodes_id=:zipcodes_id';
$stmt1 = db::db()->prepare($sql);
foreach($data AS $row)
{
try
{
$stmt1->execute($row);
$rs1 = $stmt1->fetchAll(PDO::FETCH_ASSOC);
$stmt1->closeCursor();
syslog(LOG_INFO,'$rs1: '.print_r($rs1[0],1).' '.rand());
syslog(LOG_INFO,'$rs2: '.print_r($rs2[0],1).' '.rand());
}
catch(PDOException $e){echo(sql_error($e));}
}
Note I also used named parameters instead of positional parameters, which makes it simpler to pass $row as the array of parameter values. If the keys of the array match the parameter names, you can just pass the array. In older versions of PHP you had to include the : prefix in the array keys, but you don't need that anymore.
You should use mysqlnd anyway. It has more features, it's more memory-efficient, and its license is compatible with PHP.
I am hoping for a better answer than the following. While some of these solutions might "fix" the problem, they don't answer the original question regarding what causes this error.
Set PDO::ATTR_EMULATE_PREPARES=>true (I don't wish to do this)
Set PDO::MYSQL_ATTR_USE_BUFFERED_QUERY (didn't work for me)
Use PDOStatement::fetchAll() (not always desirable)
Use $stmt->closeCursor() after each $stmt->fetch() (this mostly worked, however, I still had several cases where it didn't)
Change PHP MySQL library from php-mysql to php-mysqlnd (probably what I will do if no better answer)
I have almost same problem. My first query after connection to db return empty result and drop this error. Enabling buffer doesn't help.
My connection code was:
try {
$DBH = new PDO("mysql:host=$hostname;dbname=$db_name", $username, $password,
array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET CHARACTER SET utf8; SET NAMES utf8",
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_NUM));
}
catch(PDOException $e) { echo $e->getMessage(); }
Solution in my way was to remove initial command:
PDO::MYSQL_ATTR_INIT_COMMAND => "SET CHARACTER SET utf8; SET NAMES utf8"
Here is a correct code:
try {
$DBH = new PDO("mysql:host=$hostname;dbname=$db_name", $username, $password,
array(PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_NUM));
}
catch(PDOException $e) { echo $e->getMessage(); }
And MYSQL_ATTR_USE_BUFFERED_QUERY is not forced to true. It's set as default.
I also experienced this problem today and noticed that I put wrong SQL statement (SELECT) into PDO's exec() method. Then I came to a conclusion that we can only put write (INSERT, UPDATE, DELETE) SQL statements instead of read (SELECT) ones to the method.
!!!
WARNING !!!
This can also happen if you are trying to fetch a non SELECT query (Eg - UPDATE/INSERT/ALTER/CREATE)
if anybody is here, with error while creating tables
this also happens if you try to execute create 2 tables in single query;
this error was thrown when i fired below query;
$q24="create table if not exists table1 like template1;create table if not exists table2 like template2;";
$s24=$link->prepare($q24);
$s24->execute();
seems tables are to be created separately;
$q1="create table if not exists table1 like template1;";
$s1=$link->prepare($q1);
$s1->execute();
//and
$q2="create table if not exists table2 like template2;";
$s2=$link->prepare($q2);
$s2->execute();
I had the same problem, I was sending results to another function mid loop. Quick fix was, save all results in an array (like Bill stated, if it's too large you have other issues to worry about), after collecting the data, I ran a separate loop to call the function one at a time.
Also, PDO::MYSQL_ATTR_USE_BUFFERED_QUERY did not work for me.
I had the same problem and solved it by removing all initial requests related to the character set.
so I started from
$con = new \PDO(self::getDriver() . ":host=" . self::getHost() . ":".self::getPort()."; dbname=" . self::getName() . ";charset=utf8", self::getUser(), self::getPassword(), array( \PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,\PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8;SET SESSION time_zone ='+01:00'"));
to
$con = new \PDO(self::getDriver() . ":host=" . self::getHost() . ":".self::getPort()."; dbname=" . self::getName() . ";charset=utf8", self::getUser(), self::getPassword(), array( \PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,\PDO::MYSQL_ATTR_INIT_COMMAND => "SET SESSION time_zone ='+01:00'"));
so I removed the command SET NAMES utf8;
I got this error when I accidentally called execute twice, one explicitly and one that was hidden in another call (PDO class).
After removing the first execute, the error was gone.
The main reason behind this error is that MySQL is trying to run 'exec' instead of 'execute' and vice versa.
There are two PDO statements that are there to execute queries PDO::exec() and PDO::execute(), both are not the same.
PDO::exec() is designed to execute commands and queries that do not produce a result set.
Ex: SET, UPDATE, INSERT, DELETE etc.
PDO::execute() is designed to execute commands and queries that produce a result set.
Ex: SELECT, CALL, SHOW, OPTIMIZE, EXPLAIN etc.
If you use these commands in wrong place, your will be ended up with this error.
Solution:
Be careful where to use PDO::exec() and PDO::execute()
In my case for Laravel I changed my query from
DB::select("DELETE FROM " . env('DB_PREFIX') . 'products WHERE
product_id = ' . $product->id); // Internally Laravel will run PDO::execute()
'select' method
to
DB::table('product_currency')->where('product_id',
$product->id)->delete(); // Internally Laravel will run PDO::exec()
Hope this gives some more clarification!

SQL php Rollback does't work

I have the following in php:
try {
// INSERT FETCHED LIST INTO ARCHIVE
$stmt1 = $sql->prepare('INSERT INTO hsarchive (LIST) VALUES (?)');
$stmt1->bind_param("s",$total);
$stmt1->execute();
$stmt2 = $sql->prepare('TRUNCATE TABLE highscore');
$stmt2->execute();
$sql->rollback();
$stmt1->close();
$stmt2->close();
} catch (Exception $e) {
echo "error";
$sql->rollback();
}
Engine is InnoDB and the connection is started like:
$sql = getSQLAccess();
$sql->autocommit(false);
$sql->begin_transaction();
with getSQLAccess returning an object of the type connection with user, pw etc. in it.
No matter how I spin this, the table is truncated and the list is inserted into the archive. I tried switching around where I close the statements, and as you can see I'm currently not even committing, as I'm trying to figure out why the rollback doesnt work.
Anyone?
EDIT: So this would be the way to go, according to best answer:
try {
// INSERT FETCHED LIST INTO ARCHIVE
$stmt = $sql->prepare('INSERT INTO hsarchive (LIST) VALUES (?)');
$stmt->bind_param("s",$total);
$stmt->execute();
$stmt->close();
$stmt = $sql->prepare('DELETE FROM highscore');
$stmt->execute();
$stmt->close();
$sql->commit();
} catch (Exception $e) {
$sql->rollback();
}
DDL in transactions
Since we've figured out that there are no FK constraints to table highscore - then your issue is caused because since MySQL 5.0.3, TRUNCATE table syntax is equivalent to deletion of all rows logically but not physically
If there are no foreign key constraints to this table (your case) which restricts from doing this, MySQL will produce TRUNCATE operation via fast scheme: it will do DROP table + CREATE table. So while logically it's same to deletion of all rows, it's not the same in terms of how operation is maintained.
Why this is the difference? Because MySQL doesn't support DDL in transactions. More precise, such operations can not be rolled back. For MySQL, DDL operations will cause immediate implicit commit. That is why you see that your TRUNCATE statement: first, is committed even if you don't commit; second, rollback has no effect on it.
Solution
If you still need to rollback your operation, then, unfortunately, you'll need to use DELETE syntax instead of TRUNCATE. Unfortunately - because, obviously, DELETE is much slower than TRUNCATE, because rows will be processed one by one.

Mysql insert if not exists without unique constraint (don't want to rely on rolling back errors) (PHP + PDO)

I am making a registration page. I'm worrying about concurrency issues where two users register with the same username at the same time (know it's real rare, but hate having small flaws in code).
So my approach is, check if the username exists, and if not, insert a new row. I'm using PDO
What I've tried
I'm using transactions, but from my question here How exactly do transactions with PHP PDO work with concurrency? it appears that two transactions can read at the same time
I don't think I can use select...for update because I am not updating; in fact, I need to lock an "imaginary" row where a new entry will be added, if it does not exist already
I've tried googling some examples, but they don't seem to handle the concurrency issue mentioned above http://php.about.com/od/finishedphp1/ss/php_login_code_2.htm
Solution?
I've googled and added a UNIQUE constraint on the username field, but do not want to rely on a MySQL error to rollback the transaction. I'm no expert, but it just doesn't feel elegant to me. So is there a way to insert if not exists with pure MySQL?
Now, if this really is the way to go, I've got a couple questions. If a warning is thrown, does that stop the queries? Like will it throw the exception shown in the example here PHP + MySQL transactions examples ? Or will only a downright-error throw the exception?
Also, it seems to imply here Can I detect and handle MySQL Warnings with PHP? that warnings will show up somehow in the PHP output, but I've never had "visible MySQL errors." The question was not using PDO like in my code, but I was just wondering. Do MySQL errors and warnings make html output? Or does it only say something if I callerrorInfo()?
Thanks
UPDATE
If I had two unique fields, and did not want to allow either one to be null (since I know I could set one table to "null-able" and have two queries), is there a way to check which one messed up? Like I want to let the user know if username or email was taken. If I do a single insert, I won't know which one failed
And furthermore, if I have two statements (first insert username, check if failed, then try same for email), I can only detect one error at a time; if the username fails and rolls back, I cannot try to do the same for the email (well it would be redundantish, because even if the email exists I would have to check to see if the first query had failed) Edit: Think it can work if I use nested try...catch statements
I think youre over thinking this. You can show a different error to the user for each one but you can really only detect one at a time because mysql is only going to give you one error message with the first problem it encounters. Assuming the previous query (to insert all values at once):
try {
$stmt = $db->prepare($sql);
$stmt->execute(array(
':username' => $username,
':email' => $email,
':password' => $password
));
$db->commit();
} catch (PDOException $e) {
$db->rollBack();
if($e->getCode() == 23000) {
// lets parse the message
if(false !== strpos('email', $e->getMessage())) {
echo 'Email "'. $email . '" already exists';
} else if(false !== strpos('username', $e->getMessage()) {
echo 'Username "'. $username .'" taken';
}
} else {
// not a dupe key rethrow error
throw $e;
}
}
Now the hitch with that is the error message is going to look something like:
Duplicate entry 'THE_VALUE_YOU_TRIED_TO_INSERT' for key THE_KEY_NAME
The way to get more meaningful reporting on which column it was is to name the indexes with something meanigful when you create the table for example:
CREATE TABLE the_table (
`id` integer UNSIGNED NOT NULL AUTO_INCREMENT
`username` varchar(30),
`email` varchar(100),
`password` varchar(32),
PRIMARY KEY (`id`),
UNIQUE KEY `uk_email` (`email`),
UNIQUE KEY `uk_username` (`username`)
);
so now your error message will look like:
Duplicate entry 'THE_VALUE_YOU_TRIED_TO_INSERT' for key uk_username
OR
Duplicate entry 'THE_VALUE_YOU_TRIED_TO_INSERT' for key uk_email
Which is easily parsable.
The only real alternative to doing it this way is to do a select on table before you insert to ensure the values dont already exist.
If youre using PDO you shouldnt have PHP warnings... Just exceptions, which if uncaught will generate a standard php error. IF you have display_errors turned off then that wont be output to the screen, only to the error log.
I dont think there is a way to insert if not exists so you are back to using a unique key and then catching the exception:
$db = new PDO($dsn, $user, $pass);
$sql = "INSERT INTO the_table (username, email, etc) VALUES (:username,:email,:password)";
$db->beginTransaction();
try {
$stmt = $db->prepare($sql);
$stmt->execute(array(
':username' => $username,
':email' => $email,
':password' => $password
));
$db->commit();
} catch (PDOException $e) {
$db->rollBack();
if($e->getCode() == 23000) {
// do something to notify username is taken
} else {
// not a dupe key rethrow error
throw $e;
}
}
So is there a way to insert if not exists with pure MySQL?
The best method is generally considered to rely on the UNIQUE constrain. There are other alternatives though.
If a warning is thrown, does that stop the queries?
Not necessarily, but in your specific case trying to insert a duplicate that has the UNIQUE constrain will never get through no matter what.
Do MySQL errors and warnings make html output?
No, not automatically, you have to handle it yourself. If the operation throws an exception and you have an Exception Handler set it will naturally fire that, but it can also be handled.
Anyhow, strictly speaking an attempt to insert an entry in a table where one of the fields is duplicated and has the UNIQUE constrain will never succeded, no matter the error. It's up to you then how to handle the error, but PDO specifically throws an exception on these cases so it can be as simple as:
try {
//Insert user here
} catch(Exception $e) {
if(($PDO->errorCode() == 23000) || ($PDOStatement->errorCode() == 23000)) {
//Duplicate, show friendly error to the user and whatnot
}
}
I've used the SQLSTATE error code 23000 here because it's the most common for duplicates, but you can check for pretty much anything, here is the list of server error codes for mysql.
The best way to make sure you don't have duplicates is to use UNIQUE constraint on the username field. MySQL will definitely not insert a second row with the same value and you no longer need transactions for that.
I don't know why this solution doesn't seem elegant, but in my opinion it is a very good practice with simplifies your work a lot. If this is a constraint on data, it makes sense to let the database server handle that problem for you.
If you do the insert query in a transaction, in case of error, the execution will be stopped and the server would do a rollback. You will also get the error in PHP which you need to handle.
Let the db handle this, because its in an excellent position to do this task.
Use the unique constraint, but do
insert ignore into users...
http://dev.mysql.com/doc/refman/5.5/en/insert.html
An error wont be triggered. You can check the affected rows to see if a row was inserted.

MySQL autocommit - Inserts are ignored

I have a MySQl database with a few tables, all UTF-8 and MyISAM storage.
In PHP I am parsing an XML file which writes a lot of data to the tables. I am using just simple Insert statements and the mysqli functions.
There not so many read actions on the table and no one of them are during the inserts. First the performance was very very slow so I added SET AUTOCOMMIT = 0 at the beginning of the script.
The issue I have now is that all my inserts which are in e.g. the third foreach loop are ignored and do not appear in the mysql tables. Everything before that is fine.
So my question is, what am I doing wrong and how should I do it?
With autocommit on = Everything is inserted but very very slow
With autocommit off = Everything is very fast but a lot of inserts are ignored
Hopefully someone have an idea and can help.
MySQL is faster with autocommit turned off because INSERTs are not written to your database immediately; the data is only saved when you execute a COMMIT statement. Do you have a COMMIT statement after inserting your data?
You should try like this:
<?php
try {
$db->beginTransaction();
$stmt = $db->prepare("SOME QUERY?");
$stmt->execute(array($value1));
$stmt = $db->prepare("YET ANOTHER QUERY??");
$stmt->execute(array($value2, $value3));
$db->commit();
} catch(PDOException $ex) {
//Something went wrong then rollback!
$db->rollBack();
echo $ex->getMessage();
}
Note: calling bindTransaction() turns off auto commit automatically.
While with mysqli, you can use following instead:
mysqli_autocommit($dbh, FALSE); // turn off auto-commit
mysqli_rollback($dbh); // if error, roll back transaction
mysqli_commit($dbh); // commit transaction

Categories