I'm building a simple web app using PostgreSQL and PHP (using PDO). I've got a UNIQUE constraint on one column, and now I'm trying to come up with how best to handle an attempt to insert a duplicate into that database table.
Do I have to explicitly run a SELECT statement first and check if the value that I'm about to insert already exists, or can I handle it using PDO exceptions?
Right now I'm doing
try{
$stmt = $pdo->prepare("INSERT INTO table (column) VALUES (?) RETURNING id;");
$stmt->execute( array('duplicate') );
}
catch( PDOException $e ){
print $e;
}
which gives me
exception 'PDOException' with message 'SQLSTATE[23505]: Unique violation: 7 ERROR: duplicate key value violates unique constraint "column"
DETAIL: Key (column)=(duplicate) already exists.'
I'm thinking it would be cleaner to just be able to pick up the error when it occurs, rather than first checking if the value already exists (which sort of defeats the benefit of having the UNIQUE constraint in the first place).
In the end, I'd like to present a user friendly error message to the user, something like "The username already exists".
I suppose a hackish way would be to check the exception details and generate my friendly error messages based on that, but can I trust that the details (like SQLSTATE[23505] or code=7) won't change in the future?
Can I build a better SQL statement that returns something useful on duplicate constraint failure (and the id on success, like it is now)?
EDIT
Creating a stored procedure might be one solution:
CREATE OR REPLACE FUNCTION my_insert(a_value text)
RETURNS text AS $$
DECLARE
retval text;
BEGIN
INSERT INTO table (column) VALUES (a_value) RETURNING id INTO retval;
RETURN retval;
EXCEPTION
WHEN unique_violation THEN
retval = 'duplicate';
RETURN retval;
END;
$$ LANGUAGE plpgsql;
, then checking if the return value is 'duplicate' or an id.
While it doesn't let me utilize PHP exceptions to determine what went wrong and generate a friendly error message, it does eliminate the need for a separate SELECT-statement. Still not fully satisfied with this solution, so if you know of something even more elegant...
The SQLSTATE codes are defined in the SQL standard and are unlikely to change. They might not always provide enough information by themselves (hence the "DETAIL" section), but can be considered reliable.
Related
Is there a possibility to ask PDO whether a
CREATE TABLE IF NOT EXISTS
statement actually created a table? I am looking for something similar to check lastInsertID after an INSERT IGNORE statement. If you don't get a result for lastInsertID from PDO, then no data was inserted (because of already existing keys).
Although mysql's documentation is not great on this, the if not exists clause will result in a mysql warning if the table does exist. Unfortunately, PDO only captures mysql errors, not warnings, so you have to execute a show warnings sql statement after the create table and parse its results to check if any warning has been raised:
$warnings = $pdoconn->query("SHOW WARNINGS")->fetchObject();
// example output of $warnings OR NULL
// stdClass Object
// (
// [Level] => Warning
// [Code] => 1050
// [Message] => Table '...' already exists
// )
If no warning is raised that the table already exists and there is no other error raised for the create table statement, then the table was created as a result of the last create table.
To be honest, it may be simpler not to use the if not exists clause and then just use the standard pdo exception handling to capture the mysql error if the table exists. The if not exists clause was really meant for long sql scripts that do not have proper error handling.
I'm wondering if there's an easy way to check if an INSERT statement triggers a conflict due to a unique index on a MySQL field while performing the INSERT.
For example, say I'm adding a user to a database, and username is a unique index:
$sql = new MySQLi(...);
$res = $sql->query('INSERT INTO `users` (`username`) VALUES ("john_galt");');
A user named john_galt already exists, so the INSERT won't be performed. But how can I best respond to the user with a meaningful error (i.e. A user with that username already exists; please choose a unique name.)?
I thought of a couple things, like checking if insert_id is set -- which it shouldn't be if the query generated an error. But that error could be anything.
Similarly, I could check to see if $sql->error is not empty, except, again, that could be anything as well -- a syntax error, or a malformatted value, or whatever. Obviously I'm not going to print out the actual MySQL error for the end user.
The two solutions I can think of are:
Before running the INSERT, run a SELECT TRUE FROM ``users`` WHERE ``username`` = "john_galt"; to see if the username already exists in the database, but I feel like I added a unique constraint to the field so that I wouldn't have to do this -- kinda defeats the purpose otherwise.
strpos($sql->error, 'Duplicate entry') === 0 -- seems horribly hackish.
Is there a better method for determining this using PHP's MySQLi class?
You can use errno to get the returned error code and use that, for example:
if($sql->errno === 1062) {
//do something
}
Here's a list of error codes for MySQL 5.5: http://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html
I thought the most efficient way was to create a UNIQUE field on the table instead of selecting to check for existing values before doing anything else but this makes use of two queries. Instead with a UNIQUE field only one query is necessary because MySQL checks for you. The problem is that duplicate entry errors cause an internal server error which I cannot recover from in PHP. What do you guys suggest, what is the best way to avoid duplicate entries in a PHP & MySQL application?
Use ON DUPLICATE KEY
INSERT INTO someTable (id, amount) VALUES ($to_uid, $send_amount)
ON DUPLICATE KEY UPDATE amount = amount + $send_amount
https://dev.mysql.com/doc/refman/5.0/en/insert-on-duplicate.html
2) You can catch the duplicate key exception. PDO example:
try{
$stmt->execute(...);
}
catch(PDOException $e){
if($e->errorInfo[1] == 1062){
// Mysql returned 1062 error code which means a duplicate key
}
}
You could use REPLACE INTO for your query, it will try an insert first and than it will delete the row with the same ID and replace it.
FOUND THE SOLUTION!
CodeIgniter requires the setting
$db['default']['stricton'] = TRUE;
an explicitly calling
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
In order for MySQL to throw exceptions. The exceptions must also be caught.
You can use INSERT IGNORE to prevent updating a row and prevent an exception from being thrown if row already exists.
https://dev.mysql.com/doc/refman/5.5/en/insert.html
If you use the IGNORE keyword, errors that occur while executing the
INSERT statement are ignored. For example, without IGNORE, a row that
duplicates an existing UNIQUE index or PRIMARY KEY value in the table
causes a duplicate-key error and the statement is aborted. With
IGNORE, the row still is not inserted, but no error occurs. Ignored
errors may generate warnings instead, although duplicate-key errors do
not.
I have a unique key set for a mysql database row so not to insert duplicate entries on a form submit. That works fine, but if there's a duplicate entry the page doesn't load. Instead the user receives the warning: Duplicate entry ''' for key ''
How do I go about turning that error off and loading the page even if there is a duplicate key, while still using the unique key on the row? I tried setting error report to off, but that didn't work.
mysql_query("INSERT INTO user
(formemail,UserIP,Timestamp,LP) VALUES('$email','$userip',NOW(),'$lp') ")
or die(mysql_error());
You need to change your SQL insert to use INSERT ... ON DUPLICATE KEY UPDATE Syntax so that the error isn't generated in the first place.
Don't try to hide the symptom, treat the problem.
Also, I must point out that the mysql library is being deprecated and should not be used for new code, you should, at the least, use mysqli or, preferably PDO. There is a good tutorial on PDO here if you are interested in learning.
I have a PHP foreach loop and a mysql insert statement inside of it. The loop inserts data into my database. I've never ran into this issue before but what I think is happening is that the insert dies (I do not have an "or die" statement after the insert) when it reaches a duplicate record. Even though there may be duplicate records in the table, I need this to just continue. Is there something that I need to specify to do this?
I'm transferring some records from one table to another. Right now, I have 20 records in table #1 and only 17 in table #2. I'm missing 3 records but only one of those are duplicated which violates the constraint on the table. The other two records should have been added. Can someone give me some advice here?
What's happening is that PHP is throwing a warning when the mysql insert fails and stopping on that warning. The best way to accomplish your goal is:
Create a custom exception handler
Set PHP to use the exception handler for warnings.
Wrap the insert attempt into a try / catch
When you catch the exception / warning, either log or output the mysql error but continue script execution.
This will allow your script to continue without stopping while at the same time explaining to you the problem.
One way around this would be to simply query the database for the record that you're about to insert. This way, your series of queries will not die when attempting to insert a duplicate record.
A slightly more efficient solution would be to query for [i]all[/i] of the records you're about to insert in one query, remove all the duplicates, then insert the new ones.
Do you insert multiple rows with one INSERT statement?
INSERT INTO xyz (x,y,z) VALUES
(1,2,3),
(2,3,5),
(3,4,5),
(4,5,6)
Then you might want to consider prepared statements
...or adding the IGNORE keyword to your INSERT statement
INSERT IGNORE INTO xyz (x,y,z) VALUES
(1,2,3),
(2,3,5),
(3,4,5),
(4,5,6)
http://dev.mysql.com/doc/refman/5.0/en/insert.html says:
If you use the IGNORE keyword, errors that occur while executing the INSERT statement are treated as warnings instead
You can still fetch the warnings but the insertion will not be aborted.
Not a good way cause you should figure out whats wrong, but to just prevent it from dieing try adding # in front of the function
#mysql_query = ...
INSERT INTO FOO
(ID, BAR)
VALUES(1,2),(3,4)
ON DUPLICATE KEY UPDATE BAR=VALUES(BAR)