【问题标题】:Mysql insert if not exists without unique constraint (don't want to rely on rolling back errors) (PHP + PDO)Mysql insert if not exists without unique constraint(不想依赖回滚错误)(PHP + PDO)
【发布时间】:2012-06-25 16:24:30
【问题描述】:

我正在制作一个注册页面。我担心两个用户同时使用相同的用户名注册的并发问题(知道这非常罕见,但讨厌代码中有小缺陷)。

所以我的方法是,检查用户名是否存在,如果不存在,则插入一个新行。我正在使用 PDO

我的尝试

我正在使用事务,但从我的问题How exactly do transactions with PHP PDO work with concurrency? 看来,两个事务可以同时读取

  • 我不认为我可以使用select...for update,因为我没有更新;事实上,如果新条目不存在,我需要锁定一个“虚构”行,将添加一个新条目
  • 我尝试在谷歌上搜索一些示例,但它们似乎无法处理上面提到的并发问题http://php.about.com/od/finishedphp1/ss/php_login_code_2.htm

解决方案?

我已经搜索并在用户名字段上添加了UNIQUE 约束,但不想依赖 MySQL 错误来回滚事务。我不是专家,但它对我来说并不优雅。 那么有没有办法用纯 MySQL 来insert if not exists

现在,如果这真的是要走的路,我有几个问题。 如果抛出警告,是否会停止查询? 就像它会抛出此处示例中所示的异常 PHP + MySQL transactions examples ?还是只有彻头彻尾的错误才会抛出异常?

此外,Can I detect and handle MySQL Warnings with PHP? 似乎暗示警告会以某种方式出现在 PHP 输出中,但我从未遇到过“可见的 MySQL 错误”。问题不是在我的代码中使用 PDO,但我只是想知道。 MySQL 错误和警告是否会产生 html 输出?还是只有当我打电话给errorInfo()时才会说些什么?

谢谢

【问题讨论】:

    标签: php mysql insert pdo unique


    【解决方案1】:

    更新

    如果我有两个唯一字段,并且不想让任何一个为空(因为我知道我可以将一个表设置为“可空”并有两个查询),有没有办法检查哪一个弄乱?就像我想让用户知道是否使用了用户名或电子邮件。如果我做一个插入,我不知道哪个失败了

    此外,如果我有两个语句(首先插入用户名,检查是否失败,然后尝试相同的电子邮件),我一次只能检测一个错误;如果用户名失败并回滚,我不能尝试对电子邮件做同样的事情(这将是多余的,因为即使电子邮件存在我也必须检查第一个查询是否失败)编辑:认为它可以如果我使用嵌套的 try...catch 语句就可以工作

    我想你想多了。您可以为每个错误向用户显示不同的错误,但实际上一次只能检测一个错误,因为 mysql 只会在遇到第一个问题时给您一个错误消息。假设前面的查询(一次插入所有值):

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

    现在的问题是错误消息看起来像:

    Duplicate entry 'THE_VALUE_YOU_TRIED_TO_INSERT' for key THE_KEY_NAME

    获得更有意义的关于哪一列的报告的方法是在创建表时使用有意义的名称命名索引,例如:

    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`)
    ); 
    

    所以现在您的错误消息将如下所示:

    Duplicate entry 'THE_VALUE_YOU_TRIED_TO_INSERT' for key uk_username

    Duplicate entry 'THE_VALUE_YOU_TRIED_TO_INSERT' for key uk_email

    这很容易解析。

    这样做的唯一真正替代方法是在插入之前对表进行选择,以确保值不存在。


    如果你使用 PDO,你不应该有 PHP 警告...只是异常,如果未捕获将生成标准 php 错误。如果您关闭了display_errors,则不会输出到屏幕,只会输出到错误日志。

    我认为没有办法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;
      }
    }
    

    【讨论】:

    • 如果我有两个唯一字段,并且不想让任何一个为空(因为我知道我可以将一个表设置为“可空”并有两个查询),是否有如何检查哪一个搞砸了?就像我想让用户知道 usernameemail 是否被占用。如果我做一次插入,我不知道哪个失败了
    • 此外,如果我有两个语句(首先插入用户名,检查是否失败,然后尝试相同的电子邮件),我一次只能检测一个错误;如果用户名失败并回滚,我不能尝试对电子邮件做同样的事情(这将是多余的,因为即使电子邮件存在,我也必须检查第一个查询是否失败)编辑:如果我使用嵌套的try...catch 语句,认为它可以工作
    • @Raeki,尝试在 try catch 块内的单个查询中插入用户名和电子邮件,并在 catch 块中解析错误消息以确定用户名或电子邮件是否失败(例如使用 PDO:: errorInfo() 如果你正在使用 PDO)
    • 所以我遇到了这个烦人的错误,没有执行任何操作。我花了几个小时调试并最终缩小范围并在PDOStatement 上调用errorCode(),并发现了问题(列名拼写错误......你不讨厌吗?)。所以有一个 MySQL 错误,但它没有抛出异常。那是怎么回事?
    • 环顾四周后,我意识到这可能是因为我没有设置PDO::ERRMODE_EXCEPTION。当我这样做时,我得到PDO is not defined 或其他东西的错误。我已经包含了extension=pdo.sophp.net/manual/en/pdo.installation.php,并且在phpinfo()下启用了 MySQL 的 PDO
    【解决方案2】:

    那么如果纯 MySQL 不存在,有没有办法插入?

    一般认为最好的方法是依赖 UNIQUE 约束。不过还有其他alternatives

    如果抛出警告,是否会停止查询?

    不一定,但在您的特定情况下,尝试插入具有 UNIQUE 约束的副本无论如何都不会通过。

    MySQL 错误和警告是否会产生 html 输出?

    不,不是自动的,您必须自己处理。如果操作抛出异常并且您设置了异常处理程序,它自然会触发它,但它也可以被处理。

    无论如何,严格来说,在其中一个字段重复且具有 UNIQUE 约束的表中插入条目的尝试永远不会成功,无论错误如何。然后由您决定如何处理错误,但 PDO 在这些情况下会特别抛出异常,因此它可以很简单:

    try {
        //Insert user here
    
    } catch(Exception $e) {
        if(($PDO->errorCode() == 23000) || ($PDOStatement->errorCode() == 23000)) {
            //Duplicate, show friendly error to the user and whatnot
        }
    } 
    

    我在这里使用了 SQLSTATE 错误代码 23000,因为它是最常见的重复项,但您几乎可以检查任何内容,here 是 mysql 的服务器错误代码列表。

    【讨论】:

    • 谢谢,如果您不介意查看我上面的评论,我有一个与您和上述人的答案相关的小问题
    【解决方案3】:

    确保没有重复项的最佳方法是在用户名字段上使用UNIQUE 约束。 MySQL 绝对不会插入具有相同值的第二行,并且您不再需要为此进行事务。

    我不知道为什么这个解决方案看起来不优雅,但在我看来,这是一个非常好的做法,可以大大简化您的工作。如果这是对数据的约束,那么让数据库服务器为您处理这个问题是有意义的。

    如果您在事务中执行插入查询,如果出现错误,将停止执行,服务器将进行回滚。您还会收到需要处理的 PHP 错误。

    【讨论】:

      【解决方案4】:

      让 db 处理这个,因为它非常适合完成这项任务。

      使用唯一约束,但要这样做

      insert ignore into users...
      

      http://dev.mysql.com/doc/refman/5.5/en/insert.html

      不会触发错误。您可以检查受影响的行以查看是否插入了行。

      【讨论】:

        猜你喜欢
        • 2012-11-03
        • 1970-01-01
        • 2012-06-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-11-28
        相关资源
        最近更新 更多