-2

I want to insert a new user only, if there is no user with the same email already in the database.

I was reading this post: MySQL - ignore insert error: duplicate entry but the problem there is that after the execution of the sql statement, i don't know if the new user was successful inserted or not.

Community
  • 1
  • 1
Jo Smo
  • 6,923
  • 9
  • 47
  • 67

3 Answers3

4

after the execution of the sql statement, i don't know if it was successful or not

If I understand correctly, you want to know whether an insertion occurred or not after using INSERT IGNORE. After executing your query call LAST_INSERT_ID() and if it returns 0 it means a row was not inserted.

https://dev.mysql.com/doc/refman/5.0/en/information-functions.html#function_last-insert-id

If you use INSERT IGNORE and the row is ignored, the AUTO_INCREMENT counter is not incremented and LAST_INSERT_ID() returns 0, which reflects that no row was inserted.

Test

mysql> insert ignore into example (`id`, `data`) VALUES (2,'hello');
Query OK, 1 row affected (0.04 sec)

mysql> insert ignore into example (`id`, `data`) VALUES (3,'hello');
Query OK, 1 row affected (0.06 sec)

mysql> insert ignore into example (`id`, `data`) VALUES (2,'hello');
Query OK, 0 rows affected (0.00 sec)

mysql> select last_insert_id();
+------------------+
| last_insert_id() |
+------------------+
|                0 |
+------------------+
1 row in set (0.00 sec)

Warning

Some versions of MySQL have a bug where last_insert_id() doesn't work correctly with INSERT IGNORE: http://bugs.mysql.com/bug.php?id=67535

FuzzyTree
  • 32,014
  • 3
  • 54
  • 85
  • 1
    Excellent answer. REPLACE would bump the autoincrement id, and ON DUPLICATE KEY would probably return the current id that's stopping the insertion. – ffflabs Jul 04 '14 at 20:17
  • 2
    @tastro it would work, `LAST_INSERT_ID()` returns the data for the last query of the 'current' session (as opposed to the last global query) – FuzzyTree Jul 04 '14 at 20:25
  • 2
    I mean your answer as "> LAST_INSERT_ID() and if it returns 0 it means a row was not inserted" is not correct for some variants /please update your answer.. _others may misunderstand_ – Denis Kuzmin Jul 05 '14 at 09:33
  • 2
    bonus, when is it too may not work: you have the InnoDB table and wish to use [AUTO_INCREMENT on a secondary column in a multiple-column index](http://dev.mysql.com/doc/refman/5.6/en/example-auto-increment.html) so, we have a some stored-procedure with own AI increment for secondary field (i can show sample if you needed). That's all your variant not work again :) etc. – Denis Kuzmin Jul 05 '14 at 09:53
2

Just do a regular insert, then check the error code afterwards:

$result = mysql_query("INSERT ... stuff that causes duplicate key error");
if (mysql_errno() == 1022) {
   ... account already exists ...
}

This is somewhat safer than doing a "select" first to see if the email exists, then inserting if it doesn't. Another parallel request might "steal" the account out from under you.

The mysql error codes are all documented here: http://dev.mysql.com/doc/refman/5.0/en/error-messages-server.html


comment followup:

My answer MAY be more reliable in some cases, e.g. consider this:

mysql> create table foo (x int primary key auto_increment, y  text);
Query OK, 0 rows affected (0.00 sec)

mysql> insert into foo (y) values ('bar1'); // id #1
Query OK, 1 row affected (0.00 sec)

mysql> insert into foo (y) values ('bar2'); // id #2
Query OK, 1 row affected (0.00 sec)

mysql> insert into foo (x,y) values (2, 'bar2'); // try to create a dupe id #2
ERROR 1062 (23000): Duplicate entry '2' for key 'PRIMARY'
mysql> select last_insert_id();
+------------------+
| last_insert_id() |
+------------------+
|                2 |
+------------------+
1 row in set (0.00 sec)

Note how you STILL get a last_insert_id(). That's because it's always the value of the last SUCCESSFUL insertion. The value will not get reset just because an insert failed.

If you're doing multiple inserts with your DB handle, and try to use the if last_insert_id == 0 trick, then you may get a false answer, because you'd be getting the ID of some OTHER insert that succeeded, not this one that just failed.

And note that insert ignore doesn't help either:

mysql> insert ignore into foo (x,y) values (1, 'bar1'); // create dupe id #1
Query OK, 0 rows affected (0.00 sec)

mysql> select last_insert_id();
+------------------+
| last_insert_id() |
+------------------+
|                2 |
+------------------+
1 row in set (0.00 sec)
Marc B
  • 356,200
  • 43
  • 426
  • 500
  • 1
    no guarantees that oracle won't ever change them, but they'd have to be pretty stupid/desperate to actually do so. As for safer, see my edit above. – Marc B Jul 04 '14 at 20:21
  • You're not using `INSERT IGNORE` in your example – FuzzyTree Jul 04 '14 at 20:26
  • @FuzzyTree: yeah, just added that. – Marc B Jul 04 '14 at 20:28
  • 1
    turnaround on a last_insert_id() call is pretty minimal. that's just metadata kept inside the DB directly, and doesn't invole hitting the disk. but it would still be another query, v.s. checking something that's already present in the original insert's metadata response – Marc B Jul 04 '14 at 20:32
  • When I do `INSERT IGNORE` on a dupe I get a 0 per documentation – FuzzyTree Jul 04 '14 at 20:33
  • 1
    well, I don't. v5.1.69 here. I'll take the actual live behavior of the DB any day over what the documentation claims. Though it should probably be filed as a bug report, if they really do want the `0` behavior to be what should happens. – Marc B Jul 04 '14 at 20:35
  • 1
    http://bugs.mysql.com/bug.php?id=67535 – Marc B Jul 04 '14 at 20:37
  • 1
    Ah that explains it. I'm using a different version. Thanks – FuzzyTree Jul 04 '14 at 20:40
1

if you use PDO it's simple:

try{
   // INSERT new account
}
catch(PDOException $ex){ /* already exist -> errorInfo */ }

it work if you have:

  • UQ key for your email field
  • attr mode as $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

that's all


added:

just in case for a proper understanding - see comment:

Any DB-Layer, for example:

class TableExample
{
    ...    
    // sample with inner logic
    public function createUser(...)
    {
        ...
        $this->db->query("INSERT INTO ...");
        ...
    }

    // sample with the AR etc.
    public function updateFooBar(...)
    {
        ...
        $user->name  = 'name';
        $user->email = 'email';
        $user->save();
        ...
    }    
}

Some X-layer:

class ModelStuff
{
    ....
    public function foo()
    {
        try{
            $this->example->createUser();
            ...
            $this->example->updateFooBar();
            ...
        }
        catch(PDOException $ex){
            // TODO: e.g. ($ex->getCode() == 23000)? ...
            // all PDOException with TableExample catched here
        }
    }
}

etc.

Compiled all my comment:

Community
  • 1
  • 1
Denis Kuzmin
  • 800
  • 7
  • 14
  • 1
    @tastro [PDOException](http://php.net/manual/en/class.pdoexception.php) already aggregate the **errorInfo** (corresponds to [PDO::errorInfo](http://www.php.net/manual/en/pdo.errorinfo.php)) you can simple access as a `$e->errorInfo[1]` etc. or what are you mean ? or what's the problem ? :) – Denis Kuzmin Jul 04 '14 at 23:35
  • @tastro it's example :) you can check with simple $e->[getCode()](http://php.net/manual/en/class.pdoexception.php); //[SQLSTATE - 23000](http://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html), which occurs for a duplicate-key error **or** you may get [errorInfo](http://www.php.net/manual/en/pdo.errorinfo.php) for **more** detail about error as [PDO::errorInfo](http://www.php.net/manual/en/pdo.errorinfo.php)... see your catched exception ~ var_dump($e); or in debugger | `But this way the code is longer` Compared with ? о_О – Denis Kuzmin Jul 05 '14 at 15:16
  • `..code is longer..` @tastro It's the OOP dialect and you should **already have** the try/cath block in the some level-layer for work with DB-Layer(~[Table-Gateway](http://martinfowler.com/eaaCatalog/tableDataGateway.html), [AR](http://www.martinfowler.com/eaaCatalog/activeRecord.html), or simple "direct access" I mean - "as is" :) In your question you wrote about **PDO** ? Right ?(title & tag) So, you needed only check the catch block in your x-layer. It works **if you did** as in my answer ^ I hope you(and all) understand - this block not for every request ? or i don't know what you mean :) – Denis Kuzmin Jul 05 '14 at 16:40
  • @tastro ok, no problem, I updated answer in order to avoid confusion.. – Denis Kuzmin Jul 05 '14 at 17:38