PHP PDO 预处理:高效批量插入多行数据
2025-03-20 22:14:55
使用 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 语句看看
代码解释:
- 我们遍历
$values
数组,为每一行数据生成一个占位符字符串(比如(:val10, :val20, :val30)
)。 - 同时,把参数名和参数值放到
$params
数组里。 - 把每行的占位符字符串用逗号连接起来,形成
VALUES
子句。 - 最后,把
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)
{
//插入了至少一行数据
}
代码解释:
- 这个函数接收表名、字段名数组和数据数组作为参数。
- 它会根据字段名数组和数据数组,自动生成 SQL 语句和参数数组。
- 逻辑和手动拼接差不多,只是更通用。
- 增加了一个对数据列缺失的检查。
安全提示:
- 使用预处理语句,确保任何来自用户的输入都被正确地转义。
- 验证
$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);
代码解释:
- 直接在SQL中生成
(?, ?, ?), (?, ?, ?)
的格式. 避免了循环生成命名占位符。 - 将
$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 预处理语句一次插入多行数据。 手动拼接适合数据量小、结构固定的情况。自动拼接更通用,但要注意参数名和占位符的对应关系。如果数据库支持,可以直接使用多行插入的语法。 别忘了,大量数据插入时,最好用事务来保证数据的一致性。