【问题标题】:Inserting multiple rows at once with prepared statements使用准备好的语句一次插入多行
【发布时间】:2017-09-13 13:15:08
【问题描述】:

我想知道如何通过准备好的语句在数组中插入多个值。我已经查看了这两个(this questionthis other one)问题,但它们似乎没有做我正在尝试的事情。这就是我所拥有的:

$stmt = $this->dbh->prepare("INSERT INTO
t_virtuemart_categories_en_gb 
(category_name, virtuemart_category_id)
VALUES
(:categoryName, :categoryId)
;");

foreach($this->values as $insertData){
    $categoryName = $insertData['categoryName'];
    $categoryId = $insertData['categoryId'];
    $stmt->bindParam(':categoryName', $categoryName);
    $stmt->bindParam(':categoryId', $categoryId);
    $stmt->execute();
}

我尝试将 prepare 行放在 foreach 循环内部和外部,但它只添加了数组中的第一个键,我不明白为什么。

这是我的Connection.php 文件:

<?php
$hostname = 'localhost';
$username = 'root';
$password = '';

function connectDB ($hostname, $username, $password){
$dbh = new PDO("mysql:host=$hostname;dbname=test", $username, $password);
return $dbh;
}

try {
$dbh = connectDB ($hostname, $username, $password);
} catch(PDOException $e) {
echo $e->getMessage();
}

还有我的 Import.php 文件:

<?php
class Import{
public function __construct($dbh, $values) {
    $this->dbh = $dbh;
    $this->values = $values;
}

public function importData() {
    $stmt = $this->dbh->prepare("INSERT INTO
    t_virtuemart_categories_en_gb 
    (category_name, virtuemart_category_id)
    VALUES
    (:categoryName, :categoryId)
    ;");

    foreach($this->values as $insertData){
        $categoryName = $insertData['categoryName'];
        $categoryId = $insertData['categoryId'];
        $stmt->bindParam(':categoryName', $categoryName);
        $stmt->bindParam(':categoryId', $categoryId);
        $stmt->execute();
    }
}

}

【问题讨论】:

  • 您是否使用 print 语句调试过数组 ($this-&gt;values) 以查看每组值是否打印正常?通常我会说将prepare 放在循环之外并将参数也绑定到循环之外
  • @RamRaider 是的。我尝试做一个var_dumpecho 和所有东西,但它们为每次迭代返回所有正确的值。
  • 是只插入一次,还是count($this-&gt;values)次插入相同的数据?
  • @ishegg 只有一次。
  • var_dump($this-&gt;values); 打印什么?

标签: php pdo


【解决方案1】:

工作原理:

仅使用一条 INSERT sql 语句来添加由值对定义的多条记录。为了实现这一点,您必须在表单中构建相应的 sql 语句

INSERT INTO [table-name] ([col1],[col2],[col3],...) VALUES (:[col1],:[col2],:[col3],...), (:[col1],:[col2],:[col3],...), ...

通过遍历您的值数组。

注意事项:

  • 我希望你能理解所有。我尽可能多地发表评论。我 没有测试它,但它应该工作。也许是answer 我写了一个 不久前将为您提供有关结构的更多想法 数据访问类/函数。
  • 切勿使用“;”当你在 PHP 中定义它们时,放在 sql 语句的末尾。
  • 切勿使用一个输入标记来绑定多个值。对于要绑定的每个值,请使用唯一的命名输入标记。

祝你好运。

Connection.php

<?php

$hostname = 'localhost';
$username = 'root';
$password = '';
$port = 3306;

try {
    // Create a PDO instance as db connection to a MySQL db.
    $connection = new PDO(
            'mysql:host='. $hostname .';port='.$port.';dbname=test'
            , $username
            , $password
    );

    // Assign the driver options to the db connection.
    $connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, FALSE);
    $connection->setAttribute(PDO::ATTR_PERSISTENT, TRUE);
} catch (PDOException $exc) {
    echo $exc->getMessage();
    exit();
} catch (Exception $exc) {
    echo $exc->getMessage();
    exit();
}

导入.php:

<?php

class Import {

    /**
     * PDO instance as db connection.
     * 
     * @var PDO
     */
    private $connection;

    /**
     * 
     * @param PDO $connection PDO instance as db connection.
     * @param array $values [optional] Values list.
     */
    public function __construct(PDO $connection, array $values = array()) {
        $this->connection = $connection;
        $this->values = $values;
    }

    /**
     * Import data.
     * 
     * @return int Last insert id.
     * @throws PDOException
     * @throws UnexpectedValueException
     * @throws Exception
     */
    public function importData() {
        /*
         * Values clauses list. Each item will be
         * later added to the sql statement.
         * 
         *  array(
         *      0 => '(:categoryName0, :categoryId0)',
         *      1 => '(:categoryName1, :categoryId1)',
         *      2 => '(:categoryName2, :categoryId2)',
         *  )
         */
        $valuesClauses = array();

        /*
         * The list of the input parameters to be
         * bound to the prepared statement.
         * 
         *  array(
         *      :categoryName0 => value-of-it,
         *      :categoryId0 => value-of-it,
         *      :categoryName1 => value-of-it,
         *      :categoryId1 => value-of-it,
         *      :categoryName2 => value-of-it,
         *      :categoryId2 => value-of-it,
         *  )
         */
        $bindings = array();

        /*
         * 1) Build a values clause part for each array item,
         *    like '(:categoryName0, :categoryId0)', and 
         *    append it to the values clauses list.
         * 
         * 2) Append each value of each item to the input
         *    parameter list.
         */
        foreach ($this->values as $key => $item) {
            $categoryName = $item['categoryName'];
            $categoryId = $item['categoryId'];

            // Append to values clauses list.
            $valuesClauses[] = sprintf(
                    '(:categoryName%s, :categoryId%s)'
                    , $key
                    , $key
            );

            // Append to input parameters list.
            $bindings[':categoryName' . $key] = $categoryName;
            $bindings[':categoryId' . $key] = $categoryId;
        }

        /*
         * Build the sql statement in the form:
         *  INSERT INTO [table-name] ([col1],[col2],[col3]) VALUES 
         *  (:[col1],:[col2],:[col3]), (:[col1],:[col2],:[col3]), ...
         */
        $sql = sprintf('INSERT INTO t_virtuemart_categories_en_gb (
                    category_name,
                    virtuemart_category_id
                ) VALUES %s'
                , implode(',', $valuesClauses)
        );

        try {
            // Prepare the sql statement.
            $statement = $this->connection->prepare($sql);

            // Validate the preparing of the sql statement.
            if (!$statement) {
                throw new UnexpectedValueException('The sql statement could not be prepared!');
            }

            /*
             * Bind the input parameters to the prepared statement 
             * and validate the binding of the input parameters.
             * 
             * -----------------------------------------------------------------------------------
             * Unlike PDOStatement::bindValue(), when using PDOStatement::bindParam() the variable 
             * is bound as a reference and will only be evaluated at the time that 
             * PDOStatement::execute() is called.
             * -----------------------------------------------------------------------------------
             */
            foreach ($bindings as $key => $value) {
                // Read the name of the input parameter.
                $inputParameterName = is_int($key) ? ($key + 1) : (':' . ltrim($key, ':'));

                // Read the data type of the input parameter.
                if (is_int($value)) {
                    $inputParameterDataType = PDO::PARAM_INT;
                } elseif (is_bool($value)) {
                    $inputParameterDataType = PDO::PARAM_BOOL;
                } else {
                    $inputParameterDataType = PDO::PARAM_STR;
                }

                // Bind the input parameter to the prepared statement.
                $bound = $statement->bindValue($inputParameterName, $value, $inputParameterDataType);

                // Validate the binding.
                if (!$bound) {
                    throw new UnexpectedValueException('An input parameter could not be bound!');
                }
            }

            // Execute the prepared statement.
            $executed = $statement->execute();

            // Validate the prepared statement execution.
            if (!$executed) {
                throw new UnexpectedValueException('The prepared statement could not be executed!');
            }

            /*
             * Get the id of the last inserted row.
             */
            $lastInsertId = $this->connection->lastInsertId();
        } catch (PDOException $exc) {
            echo $exc->getMessage();
            // Only in development phase !!!
            // echo '<pre>' . print_r($exc, TRUE) . '</pre>';
            exit();
        } catch (Exception $exc) {
            echo $exc->getMessage();
            // Only in development phase !!!
            // echo '<pre>' . print_r($exc, TRUE) . '</pre>';
            exit();
        }

        return $lastInsertId;
    }

}

【讨论】:

  • 那些问号很奇怪:interno0, ?),(? Portátil1, ?),(? Proyectores2, ?),(? y fuentes3, ?),(? en red!!请显示您的 sql 语句,其中只有一个值行。
  • @Newwt 没问题,我们每个人一开始都必须是新手 ;-) 耐心地逐行阅读代码和 cmets。我提供给您的这段代码(从 sql 语句开始)是一个非常重要的代码,将来用于所有数据访问操作。我也很乐意帮助你。现在,关于具体问题,给我5分钟时间研究一下。
  • @Newwt 我发现了问题。我在代码中犯了一个错误,在循环foreach ($this-&gt;values as $key =&gt; $item) {...} 内。用我在编辑后的答案中更改的内容更改相等$valuesClauses[$key] = sprintf(...);。所以,就是那个平等。然后再试一次并提供反馈。注意:请勿在网络上发布此类敏感信息(例如您在粘贴箱中发布的信息)!最好在cmets上贴一小部分,用文字描述问题,因为cmets上层可以删除。
  • @Newwt 欢迎您。我很高兴它有效。现在,您可以实现“关注点分离”: 1) 您可以借助所谓的“查询构建器”类的相应方法构造 INSERT 语句。 2) 您可以使用所谓的“数据库适配器”类运行所有数据访问操作。您将 sql 语句和绑定数组传递给相应的类方法,她会为您完成所有准备、验证和插入数据的工作。
  • @Newwt 3) 在 Import 类中,您可以将 QueryBuilder 类和 DbAdapter 类作为构造函数参数传递,并在 importData() 方法中使用它们。就其本身而言,importData() 方法会将表名和要插入的值的数组作为参数。不久前,我在an answer of mine 中发布了一个数据库适配器类。也许对你有帮助。
【解决方案2】:

我认为必须为每次迭代分别准备和绑定语句:

if($stmt = $this->dbh->prepare("INSERT INTO t_virtuemart_categories_en_gb (category_name, virtuemart_category_id) VALUES (:categoryName, :categoryId);")){
    foreach($this->values as &$insertData){
        $stmt->bindParam(':categoryName', $insertData['categoryName']);
        $stmt->bindParam(':categoryId', $insertData['categoryId']);
        $stmt->execute();
        $stmt->close();
    }
}

【讨论】:

  • 也不行。仍然只添加第一个结果。
  • 还是什么都没有:/
  • 永远不要将未更改的准备好的语句放在循环中。它的优雅之处在于它只需要调用一次。请编辑或删除,以免未来的 SO 读者复制此答案。
  • 您将在第一次迭代结束时关闭$stmt。难怪它只添加了第一行数据。
【解决方案3】:

我建议这样做,使用 $dbh = mysqli_connect():

<?php
class Import{
public function __construct($dbh, $values) {
    $this->dbh = $dbh;
    $this->values = $values;
}

public function importData() {
    $stmt = $this->dbh->prepare("INSERT INTO t_virtuemart_categories_en_gb 
    (category_name, virtuemart_category_id)
    VALUES
    (?, ?)");

    $catetoryName = ''; $categoryId = '';
    $stmt->bind_param('ss', $categoryName, $categoryId);
    foreach($this->values as $insertData){
        $categoryName = $insertData['categoryName'];
        $categoryId = $insertData['categoryId'];
        $stmt->execute();
    }
}

}

通过这种方式,您可以创建一个引用并将该变量绑定到准备好的语句的执行。

传递数组时要小心,技巧在php.net页面中注释(http://php.net/manual/it/mysqli.prepare.php

有效的代码是:

$typestring = 'sss'; //as many as required: calc those
$stmt = $dbconni->prepare($ansqlstring);
$refs = [$typestring];
// if $source is an array of array  [ [f1,f2,...], [f1,f2,...], ...]
foreach($source as $data) {
  if (count($refs)==1) {
    foreach ($data as $k => $v) {
      $refs[] = &$data[$k];
    }
    $ref = new \ReflectionClass('mysqli_stmt');
    $method = $ref->getMethod("bind_param");
    $method->invokeArgs($stmt, $refs);
    $r = $stmt->execute();
  } else {
    // references are maintained: no needs to bind_param again
    foreach ($data as $k => $v) {
      $refs[$k+1] = $v;
    }
    $r = $stmt->execute();
  }
}

这个备用资源,性能更高,但你必须做基准才能确定我的话。

这是准备好的语句有意义的情况之一,请参阅

https://joshduff.com/2011-05-10-why-you-should-not-be-using-mysqli-prepare.md

通常 PDO 模拟准备好的语句,请参阅 PDO::ATTR_EMULATE_PREPARES

编辑:指定它正在使用 mysqli,并更正代码。

【讨论】:

    猜你喜欢
    • 2016-03-07
    • 1970-01-01
    • 2013-10-16
    • 2012-01-11
    • 1970-01-01
    • 2013-11-09
    • 2012-02-16
    • 2015-03-02
    • 2019-09-24
    相关资源
    最近更新 更多