【问题标题】:PDO Prepared Statement "Invalid Parameter Number"PDO 准备语句“无效的参数号”
【发布时间】:2018-07-04 04:39:55
【问题描述】:

我正在使用一个一直在处理我的代码的函数,现在我正在慢慢地从 MySQLi 迁移到 PDO - 这个函数应该“简单地”获取一条 SQL 语句和一组变量并设置准备好的语句,然后执行它并返回一个带有“成功/失败”代码的数组,然后是 lastInsertId。

到目前为止,该功能一直运行良好,考虑到我遇到的错误(而且我是 PDO 的新手),我想知道问题出在哪里。

首先,函数本身是相当良性的(常量在别处定义)

function dbConnect(){
    try {
        if(!defined('PDO::ATTR_DRIVER_NAME')){
            debugPrint('Error: PDO unavailable');
            return false;
        }

        $dsn = 'mysql:host=' . DB_SERVER . ';dbname=' . DB_NAME . ';charset=UTF8';

        $opt = [
            PDO::ATTR_EMULATE_PREPARES => false,
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
        ];

        $db = new PDO($dsn, DB_USER, DB_PASS, $opt);
        return $db;
    } catch (PDOException $e) {
        debugPrint($e->getMessage());
        return false;
    }
}


function dbInsert($sql, $param){
    $ret = [];
    if(!$pdo = dbConnect()){
        $ret[] = 1;
        return $ret;
    }

    if(!$stmt = $pdo->prepare($sql){
        echo $pdo->errorInfo();
        $ret[] = 1;
        return $ret;
    }

    if(!$stmt->execute($param)){
        echo $pdo->errorInfo();
        $ret[] = 1;
        return $ret;
    } else {
        $ret[] = 0;
        $ret[1] = $pdo->lastInsertId();
        return $ret;
    }

我正在编写代码,它只是检查电子邮件地址是否出现在表格中,如果没有,则插入它。我在我的应用程序中进行了很多检查,大约 50% 的时间提供的电子邮件将是现有条目。

$sql = 'insert into customer (email) select :email from DUAL where not exists (select 1 from customer where email = :email)';
$bind = [':email' => 'user@bogus.org'];

这是迄今为止我见过的最简洁的方法,它是对数据库的一次调用并使用准备好的语句。但是,我收到 PDO 错误“无效参数号”和堆栈跟踪错误。我已经打开了 MySQL 日志记录,看起来准备工作正常...

Prepare   insert into customer (email) select ? from DUAL where not exists (select 1 from customer where email = ?)

...所以我只能认为它是绑定,考虑到参数错误的数量,这是有道理的,但异常似乎来自执行:

PHP Fatal error:  Uncaught PDOException: SQLSTATE[HY093]: Invalid parameter number in {filename}:78
Stack trace:
    #0 {filename}:78: PDOStatement->execute(Array)
    #1 {callingFile(33)}: dbInsert('insert into cus...', Array)
    #2 {main}
        thrown in {filename} on line 78

由于我是 PDO 领域的新手并且热衷于学习,我想找出我应该在哪里解决这个问题。

我很喜欢 PDO 的功能,我希望这不是一个需要解决的太大问题,但目前我不知道为什么会抛出这个错误。我确实想知道是不是因为我使用的是“1”

注意:1 我知道我可以使电子邮件在表中唯一,但这意味着 $pdo->execute 返回非零,使我的函数返回非零(错误),这(至少在我看来)并不完全正确。

注意:2我也知道我可以先检查表格以查看电子邮件是否存在,如果不存在则插入它:但是,我不确定这是最好的做事的方式(并且由于我当前正在更新的代码这样做,我认为使其成为 PDO 以及可能对 SQL 调用本身更聪明)。

注意:3我还将 SQL 语句更改为以下内容:

$sql = 'insert ignore into customer (email) values (:email)';
$bind = [':email' => 'user@bogus.org'];

这是“OK”,但是如果存在现有电子邮件,则收件人表中的 auto_incremented 主键会跳过(再次尝试保持一切清洁、简单和合乎逻辑),我真的不想破解这个通过附加一个(慢)语句来更改表以将 auto_increment 设置为 1

【问题讨论】:

    标签: mysql pdo prepared-statement


    【解决方案1】:

    1) 在您的函数 dbInsert 中,您错过了 if 语句的最后一个括号:

    if (!$stmt = $pdo->prepare($sql)) {...}
    


    2) 如PDO::prepare 的官方文档所述:

    您不能使用同名的命名参数标记超过 一次在准备好的语句中,除非仿真模式打开。

    例如不允许在sql语句中多次使用:email,除非激活ATTR_EMULATE_PREPARES选项:

    PDO::ATTR_EMULATE_PREPARES => TRUE
    

    如果您不这样做,您将收到您提出的错误。

    所以,要么应用上面的建议,要么让模拟保持不变(例如 FALSE)并使用两个不同的命名参数标记(:email1:email2):

    $email = 'user@bogus.org';
    
    $sql = 'insert into customer (email) select :email1 from DUAL where not exists (select 1 from customer where email = :email2)';
    
    $bind = [
        ':email1' => $email,
        ':email2' => $email,
    ];
    

    或者您可以使用问号 (?) 参数标记:

    $email = 'user@bogus.org';
    
    $sql = 'insert into customer (email) select ? from DUAL where not exists (select 1 from customer where email = ?)';
    
    $bind = [
        1 => $email,
        2 => $email,
    ];
    

    作为一个很好的资源我推荐你this教程。

    【讨论】:

    • 啊!我希望这是一件简单的事情,我的经验不足。非常感谢您的帮助。
    • 不客气。请注意,参数标记只能在 sql 语句中的某些地方使用。如果它们没有用在应该使用的地方,sql注入就无法不费力地避免,或者根本无法避免。我记得,教程也指出了这个问题。
    • 请允许我问一下:我设置了一些东西(dbInsert 调用 dbConnect)阻止我使用 PDO 事务,对吗?我有一个执行插入、选择、插入和最后选择的文件,并且该文件希望确保一切正常,该文件应该产生 $pdo->beginTransaction() 并最终产​​生 $pso->提交(),是吗?当然,目前它没有 $pdo 对象,因为它在初始 dbInsert 中被调用,所以我认为在函数中调用 dbConnect 的想法需要删除,并从文件本身调用?
    • @bnoeafk 好吧,我从来没有处理过交易。但是您绝对不应该创建数据库连接,例如一个 pdo 对象,每次执行数据库操作 (CRUD) 时。相反,您应该只创建一个数据库连接对象,它应该由所有需要它的函数和类共享。例如。这个 pdo 对象应该作为参数注入函数或类构造函数中,或者在涉及到 vanilla 代码片段时按原样使用。在您的情况下,正如您已经正确建议的那样,您应该调用 dbConnect 与任何函数分开。所以,我会这样做:
    • @bnoeafk 我将在其中创建一个 php 文件 (connection.php) 和一个新的 pdo 对象(如果您愿意,$pdo),而不使用任何函数。只是来自dbConnecttry-catch 块。实际上你不需要这样的异常处理,例如try-catch 块,完全用于此类操作。阅读thisthis 了解原因和方法。然后我会在所有需要数据库操作的文件中包含文件connection.php,例如一个数据库连接。
    最近更新 更多