简体   繁体   English

如果没有唯一约束(不希望依赖回滚错误)不存在,则使用MySQL插入(PHP + PDO)

[英]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 我正在使用PDO

What I've tried 我尝试过的

I'm using transactions, but from my question here How exactly do transactions with PHP PDO work with concurrency? 我正在使用事务,但是从我的问题这里开始,PHP PDO的事务如何与并发一起工作? 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; 我不认为可以使用select...for update因为我没有更新。 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 我尝试使用Google搜索一些示例,但它们似乎无法处理上述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. 我已经用谷歌搜索并在用户名字段上添加了UNIQUE约束,但是不想依靠MySQL错误来回滚事务。 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? 那么insert if not exists纯MySQL insert if not exists ,有没有办法insert if not exists

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 ? 像它会抛出此处示例中所示的PHP + MySQL事务示例异常吗? Or will only a downright-error throw the exception? 还是只有彻底的错误才会引发异常?

Also, it seems to imply here Can I detect and handle MySQL Warnings with PHP? 另外,这似乎暗示我可以用PHP检测和处理MySQL警告吗? that warnings will show up somehow in the PHP output, but I've never had "visible MySQL errors." 该警告将以某种方式显示在PHP输出中,但我从未遇到过“可见的MySQL错误”。 The question was not using PDO like in my code, but I was just wondering. 问题不像我的代码中那样使用PDO,但我只是想知道。 Do MySQL errors and warnings make html output? MySQL错误和警告会使html输出吗? Or does it only say something if I call errorInfo() ? 还是只在我调用errorInfo()时才说些什么?

Thanks 谢谢

So is there a way to insert if not exists with pure MySQL? 那么如果纯MySQL不存在,有没有办法插入?

The best method is generally considered to rely on the UNIQUE constrain. 通常认为最佳方法依赖于UNIQUE约束。 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. 不一定,但是在您的特定情况下,尝试插入具有UNIQUE约束的重复项将永远不会成功。

Do MySQL errors and warnings make html output? MySQL错误和警告会使html输出吗?

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: 然后由您决定如何处理错误,但是PDO专门针对这些情况抛出异常,因此它可以很简单:

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. 我在这里使用了SQLSTATE错误代码23000,因为它是重复项中最常见的代码,但是您可以检查几乎所有内容, 是mysql服务器错误代码的列表。

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? 如果我有两个唯一字段,并且不想让任何一个字段都为null(因为我知道我可以将一个表设置为“ null-able”并有两个查询),那么有没有办法检查哪个被弄乱了? 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 如果用户名失败并回滚,我将无法尝试对电子邮件执行相同的操作(这将是多余的,因为即使电子邮件存在,我也必须检查第一个查询是否失败)编辑:认为可以如果我使用嵌套的try ... catch语句,则可以工作

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. 您可以为每个用户显示一个不同的错误,但实际上一次只能检测到一个错误,因为mysql只会向您提供一条错误消息,提示它遇到的第一个问题。 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. 如果您使用PDO,则不应有PHP警告...只是异常,如果未捕获,则会生成标准的php错误。 IF you have display_errors turned off then that wont be output to the screen, only to the error log. 如果关闭了display_errors ,则不会将其输出到屏幕,仅输出到错误日志。

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: 我不认为有一种方法可以insert if not exists因此您可以返回使用唯一键然后捕获异常的方法:

$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;
  }
}

The best way to make sure you don't have duplicates is to use UNIQUE constraint on the username field. 确保没有重复的最佳方法是在用户UNIQUE上使用UNIQUE约束。 MySQL will definitely not insert a second row with the same value and you no longer need transactions for that. MySQL绝对不会插入具有相同值的第二行,因此您不再需要事务。

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. 您还将得到需要处理的PHP错误。

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 http://dev.mysql.com/doc/refman/5.5/zh-CN/insert.html

An error wont be triggered. 错误不会被触发。 You can check the affected rows to see if a row was inserted. 您可以检查受影响的行以查看是否插入了行。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM