返回

PHP PDO 预处理:高效批量插入多行数据

mysql

使用 PDO 预处理语句插入多行数据

有时候我们需要一次性插入多条数据到数据库,用循环单条插入效率太低。那有没有办法使用 PDO 预处理语句来实现一次插入多行数据呢?这篇博客就来好好聊聊这个问题。

一、问题剖析:为啥不能直接用?

我们平时用 PDO 预处理插入单行数据,大概是这样写的:

$params = array();
$params[':val1'] = "val1";
$params[':val2'] = "val2";
$params[':val3'] = "val3";
$sql = "INSERT INTO table (col1, col2, col3) VALUES (:val1, :val2, :val3)";  //这里修正了一下原提问中的SQL语法错误
$stmt = DB::getInstance()->prepare($sql);
$stmt->execute($params);

如果我们想插入多行,比如有这样一个数组:

$values[0]['val1'] = 'a1';
$values[0]['val2'] = 'a2';
$values[0]['val3'] = 'a3';
$values[1]['val1'] = 'b1';
$values[1]['val2'] = 'b2';
$values[1]['val3'] = 'b3';
// ... 更多数据

直接把这个数组塞到 $stmt->execute() 里,肯定是不行的。PDO 预处理语句在执行时,会把数组里的键和 SQL 语句中的占位符对应起来。这种方式只适合插入单行。 如果我们有多行, 则需要在SQL中拼接多个values后的括号部分。

二、解决方案:手动/自动 拼接 SQL

既然直接不行,我们就得想办法把 SQL 语句和参数数组都处理一下,让它们能适应多行插入。

1. 手动拼接 SQL (适用于少量数据, 且数据结构已知)

如果插入的行数不多,而且数据的结构比较固定,我们可以手动拼接 SQL 语句和参数数组。

原理:

直接构建包含多个值的 VALUES 子句的 SQL 字符串,并构建相匹配的参数数组。

代码示例:

$values = [
    ['val1' => 'a1', 'val2' => 'a2', 'val3' => 'a3'],
    ['val1' => 'b1', 'val2' => 'b2', 'val3' => 'b3'],
];

$sql = "INSERT INTO table (col1, col2, col3) VALUES ";
$params = [];
$placeholders = [];

foreach ($values as $index => $row) {
    $rowPlaceholders = [];
    foreach ($row as $key => $value) {
        $paramKey = ':' . $key . $index; // 生成唯一的参数名
        $rowPlaceholders[] = $paramKey;
        $params[$paramKey] = $value;
    }
    $placeholders[] = '(' . implode(', ', $rowPlaceholders) . ')';
}

$sql .= implode(', ', $placeholders);

$stmt = DB::getInstance()->prepare($sql);
$stmt->execute($params);

// echo $sql; // 可以打印出生成的 SQL 语句看看

代码解释:

  1. 我们遍历 $values 数组,为每一行数据生成一个占位符字符串(比如 (:val10, :val20, :val30))。
  2. 同时,把参数名和参数值放到 $params 数组里。
  3. 把每行的占位符字符串用逗号连接起来,形成 VALUES 子句。
  4. 最后,把 VALUES 子句拼接到 SQL 语句中,执行预处理语句。

安全提示:
即使是手动拼接,也要保证value部分是参数化的。避免手动去拼接value。

2. 自动拼接 SQL (更通用)

如果插入的行数比较多,或者数据的结构不固定,手动拼接就太麻烦了。我们可以写一个函数来自动拼接。

原理:

函数接收表名、字段名数组、数据数组作为参数,自动生成 SQL 和参数数组。

代码示例:

function insertMultipleRows($tableName, $columns, $data) {
    $sql = "INSERT INTO $tableName (" . implode(', ', $columns) . ") VALUES ";
    $params = [];
    $placeholders = [];

    foreach ($data as $index => $row) {
        $rowPlaceholders = [];
        foreach ($columns as $column) {
             if (isset($row[$column]))
             {
                $paramKey = ':' . $column . $index;
                $rowPlaceholders[] = $paramKey;
                $params[$paramKey] = $row[$column];
             }
             else
             {
                //处理列不存在的情况, 比如直接报错
                throw new Exception("Column '$column' not found in row $index");
             }
        }
        $placeholders[] = '(' . implode(', ', $rowPlaceholders) . ')';
    }

    $sql .= implode(', ', $placeholders);

    $stmt = DB::getInstance()->prepare($sql);
    $stmt->execute($params);
    return $stmt; //返回结果, 用于判断插入影响行数等等

}

// 使用示例
$tableName = 'table';
$columns = ['col1', 'col2', 'col3'];
$data = [
    ['col1' => 'a1', 'col2' => 'a2', 'col3' => 'a3'],
    ['col1' => 'b1', 'col2' => 'b2', 'col3' => 'b3'],
    ['col1' => 'c1', 'col2' => 'c2', 'col3' => 'c3'],
];

$result = insertMultipleRows($tableName, $columns, $data);
// 检查插入是否成功
if ($result->rowCount() > 0)
{
  //插入了至少一行数据
}

代码解释:

  1. 这个函数接收表名、字段名数组和数据数组作为参数。
  2. 它会根据字段名数组和数据数组,自动生成 SQL 语句和参数数组。
  3. 逻辑和手动拼接差不多,只是更通用。
  4. 增加了一个对数据列缺失的检查。

安全提示:

  • 使用预处理语句,确保任何来自用户的输入都被正确地转义。
  • 验证 $tableName$columns: 虽然预处理语句能防止 SQL 注入,但恶意用户仍然可能尝试传入不存在的表名或列名,导致错误。

3. 利用数据库的特性 (如果数据库支持)

有些数据库(比如 MySQL)支持 INSERT ... VALUES (), (), ... 这种语法,可以一次插入多行数据。我们可以利用这个特性来简化操作。

原理:

直接使用数据库支持的多行插入语法。

代码示例 (MySQL):

function insertMultipleRowsMySQL($tableName, $columns, $data) {
    if (empty($data)) {
        return; // 或者抛出异常, 根据实际需求
    }

    $sql = "INSERT INTO $tableName (" . implode(', ', $columns) . ") VALUES ";
    $placeholders = [];
    $params = [];

    // 获取一行数据作为模板,用于构建占位符
    $firstRow = reset($data);  //reset取得数组中第一个元素
    $rowPlaceholder = '(' . implode(', ', array_fill(0, count($firstRow), '?')) . ')'; //(?, ?, ?)

     //根据行数增加(?, ?, ?)
    $placeholders = array_fill(0, count($data), $rowPlaceholder);

    $sql .= implode(', ', $placeholders);

    //展开所有值到一个一维数组, 作为PDO::execute的参数
     foreach($data as $row)
     {
      $params = array_merge($params, array_values($row));
     }


    $stmt = DB::getInstance()->prepare($sql);

    $stmt->execute($params);
    return $stmt;
}
// 使用示例,注意,此处使用?, 且 $data 数组需要是索引数组:
$tableName = 'table';
$columns = ['col1', 'col2', 'col3'];
$data = [
    ['a1', 'a2', 'a3'],  // 注意这里的格式!
    ['b1', 'b2', 'b3'],
    ['c1', 'c2', 'c3'],
];
$result = insertMultipleRowsMySQL($tableName, $columns, $data);

代码解释:

  1. 直接在SQL中生成 (?, ?, ?), (?, ?, ?) 的格式. 避免了循环生成命名占位符。
  2. $data 中的所有值展开成一个一维数组。 因为我们使用了 ? 作为占位符,所以直接把所有数据展开到一个数组里就行了。

安全提示:

  • 确保 tableName, columns 来自可信源, 做好白名单过滤。
  • 这种方式依然要用到prepare。不要手动去拼任何value部分。

进阶:事务

对于大量数据的插入,强烈建议使用事务。 这样可以保证所有数据要么全部插入成功,要么全部失败回滚,避免出现部分数据插入成功、部分失败的情况。

try {
    DB::getInstance()->beginTransaction();
    // ... 执行插入操作 ... (使用上面任何一个方法都可以)
     insertMultipleRows($tableName, $columns, $data);  //示例
    DB::getInstance()->commit();
} catch (Exception $e) {
    DB::getInstance()->rollBack();
    // 处理异常
    echo "插入失败: " . $e->getMessage();
}

三、总结

这篇博客介绍了如何使用 PDO 预处理语句一次插入多行数据。 手动拼接适合数据量小、结构固定的情况。自动拼接更通用,但要注意参数名和占位符的对应关系。如果数据库支持,可以直接使用多行插入的语法。 别忘了,大量数据插入时,最好用事务来保证数据的一致性。