PDO连金仓数据库(下篇):预处理语句、大对象和批量操作
接上篇
上篇讲了PDO连接金仓数据库的基础配置和CRUD操作。这篇说点生产中会用到的------预处理语句的细节、大对象的存储和读取、以及用COPY命令批量导入导出数据。
这些都是我在项目中真实用到的功能,今天把它们整理出来。
一、预处理语句:不只是防SQL注入
1.1 为什么要用预处理
很多人以为预处理就是为了防SQL注入。其实还有一个更重要的好处:性能。
同样的SQL执行多次,数据库只需要解析一次,后面直接复用执行计划。循环插入1000条数据,用预处理和不用的差别非常明显。
先看不用预处理的写法:
php
for ($i = 0; $i < 1000; $i++) {
$pdo->query("INSERT INTO logs (message) VALUES ('log $i')");
}
每次循环,数据库都要解析SQL语句,1000次就是1000次解析。
用预处理的写法:
php
$stmt = $pdo->prepare("INSERT INTO logs (message) VALUES (?)");
for ($i = 0; $i < 1000; $i++) {
$stmt->execute(["log $i"]);
}
SQL只解析一次,后面只传参数。在高并发场景下,这个差别是秒级的。
1.2 位置占位符 vs 命名占位符
PDO支持两种占位符。
位置占位符用问号:
php
$stmt = $pdo->prepare("INSERT INTO users (name, email, age) VALUES (?, ?, ?)");
$stmt->execute(['张三', 'zhangsan@test.com', 25]);
参数顺序必须和SQL里的问号顺序一致。好处是写法简单,坏处是参数多了容易搞混。
命名占位符用冒号加名字:
php
$stmt = $pdo->prepare("INSERT INTO users (name, email, age) VALUES (:name, :email, :age)");
$stmt->execute([
':name' => '张三',
':email' => 'zhangsan@test.com',
':age' => 25
]);
不用管顺序,可读性也更好。参数多的时候推荐用这种。
1.3 绑定参数 vs 直接传数组
execute()可以直接传数组,也可以用bindParam()或bindValue()绑定参数。
php
$stmt = $pdo->prepare("INSERT INTO users (name, email) VALUES (?, ?)");
// 方式1:execute传数组
$stmt->execute(['张三', 'zhangsan@test.com']);
// 方式2:bindParam绑定
$name = '张三';
$email = 'zhangsan@test.com';
$stmt->bindParam(1, $name);
$stmt->bindParam(2, $email);
$stmt->execute();
// 方式3:bindValue绑定
$stmt->bindValue(1, '张三');
$stmt->bindValue(2, 'zhangsan@test.com');
$stmt->execute();
bindParam和bindValue的区别:bindParam绑定的是变量引用,变量值改变后再次执行,用的是新值。bindValue绑定的是固定值。
批量插入的时候可以用bindParam,循环里只改变量值,不用重复绑定:
php
$stmt = $pdo->prepare("INSERT INTO logs (message) VALUES (?)");
$msg = '';
$stmt->bindParam(1, $msg);
for ($i = 0; $i < 1000; $i++) {
$msg = "log number $i";
$stmt->execute();
}
这种方式比循环里execute([$msg])稍微快一点。
1.4 获取插入的自增ID
php
$stmt = $pdo->prepare("INSERT INTO users (name) VALUES (?)");
$stmt->execute(['张三']);
$id = $pdo->lastInsertId();
echo "刚插入的用户ID是:$id";
lastInsertId()返回的是当前连接上一次INSERT生成的自增值。注意它和表名、序列名无关,就是取的最后一个。
二、大对象操作
2.1 什么是大对象
大对象(LOB,Large Object)用来存图片、PDF、长文本这类数据。金仓支持两种:
- BLOB:存二进制数据(图片、文件)
- CLOB:存大段文本
2.2 用PDO::PARAM_LOB存图片
php
// 建表
$pdo->exec("CREATE TABLE test_blob (
id INTEGER PRIMARY KEY,
blob_data BLOB,
clob_data CLOB
)");
// 读图片文件
$imagePath = './test.png';
$fp = fopen($imagePath, 'rb');
// 准备插入
$stmt = $pdo->prepare("INSERT INTO test_blob (id, blob_data, clob_data) VALUES (?, ?, ?)");
$stmt->bindParam(1, $id);
$stmt->bindParam(2, $fp, PDO::PARAM_LOB); // 关键:用PARAM_LOB绑定文件句柄
$stmt->bindParam(3, $text, PDO::PARAM_STR);
$id = 1;
$text = '这是一段很长的CLOB文本,可以存几万字符...';
$pdo->beginTransaction();
$stmt->execute();
$pdo->commit();
fclose($fp);
关键点:PDO::PARAM_LOB告诉PDO这个参数是个大对象,PDO会把文件流式写入数据库,不会把整个文件读进内存。
2.3 读大对象
php
$stmt = $pdo->prepare("SELECT blob_data, clob_data FROM test_blob WHERE id = ?");
$stmt->execute([1]);
$stmt->bindColumn(1, $blob, PDO::PARAM_LOB);
$stmt->bindColumn(2, $clob, PDO::PARAM_LOB);
$stmt->fetch(PDO::FETCH_BOUND);
// 存图片
$outFp = fopen('./output.png', 'wb');
stream_copy_to_stream($blob, $outFp);
fclose($outFp);
// 存文本
file_put_contents('./output.txt', $clob);
stream_copy_to_stream是流式拷贝,不会把整个大对象读进内存,处理几百MB的文件也能扛住。
2.4 大对象的三个专用方法
PDO_KDB扩展提供了专门的大对象操作方法,比用PARAM_LOB更直接。
创建大对象:
php
$oid = $pdo->kdbLOBCreate();
打开大对象流:
php
$stream = $pdo->kdbLOBOpen($oid, 'rb');
删除大对象:
php
$pdo->kdbLOBUnlink($oid);
完整示例:
php
// 创建
$oid = $pdo->kdbLOBCreate();
// 写入
$stream = $pdo->kdbLOBOpen($oid, 'wb');
fwrite($stream, '要存的内容');
fclose($stream);
// 读取
$stream = $pdo->kdbLOBOpen($oid, 'rb');
$content = stream_get_contents($stream);
fclose($stream);
// 删除
$pdo->kdbLOBUnlink($oid);
这种方式的优点是更灵活,可以单独创建和管理大对象,不一定要跟表关联。但大多数场景用PARAM_LOB就够了。
三、COPY命令:批量导入导出
3.1 为什么需要COPY
普通INSERT插10000条数据,要执行10000次SQL。COPY命令一次就把整个数组或文件的内容塞进数据库,网络交互少、解析开销小。
金仓PDO_KDB驱动提供了四个COPY相关的方法。
3.2 从数组复制到表:kdbCopyFromArray
php
// 准备数据
$rows = [
[1, '张三', 'zhangsan@test.com'],
[2, '李四', 'lisi@test.com'],
[3, '王五', 'wangwu@test.com']
];
$pdo->kdbCopyFromArray('users', $rows, ',', 'NULL');
// 参数说明:
// 第1个参数:表名
// 第2个参数:数据数组
// 第3个参数:列分隔符(默认tab,这里用逗号)
// 第4个参数:NULL值的表示方式(默认\N)
3.3 从文件复制到表:kdbCopyFromFile
php
// 文件data.csv内容:
// 1,张三,zhangsan@test.com
// 2,李四,lisi@test.com
// 3,王五,wangwu@test.com
$pdo->kdbCopyFromFile('users', '/path/to/data.csv', ',', 'NULL');
文件里的每一行对应表里的一行。这个方式比从数组复制更快,因为数据已经在磁盘上了。
3.4 从表复制到数组:kdbCopyToArray
php
$rows = $pdo->kdbCopyToArray('users', ',', 'NULL');
foreach ($rows as $row) {
print_r($row);
}
注意:如果表数据量很大,这个方法会把所有数据读到内存,可能会撑爆。大表导出建议用下面的文件方式。
3.5 从表复制到文件:kdbCopyToFile
php
$pdo->kdbCopyToFile('users', '/path/to/export.csv', ',', 'NULL');
百万级数据导出,用COPY比SELECT一行行fetch快得多。实测过,100万行数据,COPY方式导出的时间大约是SELECT方式的十分之一。
3.6 COPY的性能对比
简单测了一下,插入1万行数据:
| 方式 | 耗时 |
|---|---|
| 普通INSERT循环 | 约2.5秒 |
| 预处理+批量execute | 约0.8秒 |
| kdbCopyFromArray | 约0.05秒 |
COPY的优势非常明显。数据迁移、日志导入这种大批量场景,强烈推荐用COPY。
四、错误处理的最佳实践
4.1 设置错误模式为异常
默认情况下PDO出错只返回false,不报错,很难排查问题。
php
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
这样设置后,任何数据库错误都会抛出PDOException异常,可以用try-catch捕获。
4.2 错误码判断
php
try {
$stmt = $pdo->prepare("INSERT INTO users (id, name) VALUES (?, ?)");
$stmt->execute([1, '张三']);
} catch (PDOException $e) {
// 错误码23505表示唯一约束冲突
if ($e->errorInfo[1] == 23505) {
echo "ID已经存在了,不能重复插入";
} else {
echo "其他错误: " . $e->getMessage();
}
}
$e->errorInfo是一个数组:
- errorInfo0:SQLSTATE错误码(5个字符)
- errorInfo1:驱动层错误码
- errorInfo2:具体错误信息
常见的金仓错误码:
| 错误码 | 含义 |
|---|---|
| 23505 | 唯一约束冲突 |
| 23503 | 外键约束失败 |
| 42P01 | 表不存在 |
| 42703 | 列不存在 |
| 08006 | 连接断开 |
4.3 长时脚本的内存管理
处理大结果集时,用fetch()而不是fetchAll():
php
// 可能撑爆内存
$rows = $pdo->query("SELECT * FROM big_table")->fetchAll();
// 逐行处理,内存安全
$stmt = $pdo->query("SELECT * FROM big_table");
while ($row = $stmt->fetch()) {
// 处理一行,扔掉一行
processRow($row);
}
另外,可以禁用Prepared Statement的仿真模式来减少内存:
php
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
五、一个完整的生产示例
php
<?php
class KingbaseDB {
private $pdo;
public function __construct($config) {
$dsn = "kdb:host={$config['host']};dbname={$config['dbname']};port={$config['port']}";
$this->pdo = new PDO($dsn, $config['user'], $config['password'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]);
}
public function query($sql, $params = []) {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt;
}
public function fetchOne($sql, $params = []) {
$stmt = $this->query($sql, $params);
return $stmt->fetch();
}
public function fetchAll($sql, $params = []) {
$stmt = $this->query($sql, $params);
return $stmt->fetchAll();
}
public function insert($table, $data) {
$fields = array_keys($data);
$placeholders = ':' . implode(', :', $fields);
$sql = "INSERT INTO $table (" . implode(',', $fields) . ") VALUES ($placeholders)";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($data);
return $this->pdo->lastInsertId();
}
public function copyFromFile($table, $file, $delimiter = ',') {
return $this->pdo->kdbCopyFromFile($table, $file, $delimiter, 'NULL');
}
public function beginTransaction() {
$this->pdo->beginTransaction();
}
public function commit() {
$this->pdo->commit();
}
public function rollback() {
$this->pdo->rollBack();
}
}
// 使用示例
$db = new KingbaseDB([
'host' => '127.0.0.1',
'port' => 54321,
'dbname' => 'TEST',
'user' => 'SYSTEM',
'password' => '123456'
]);
// 插入一条
$id = $db->insert('users', ['name' => '张三', 'email' => 'test@test.com']);
// 查询一条
$user = $db->fetchOne("SELECT * FROM users WHERE id = ?", [$id]);
// 批量导入CSV
$db->copyFromFile('users', '/tmp/users.csv', ',');
?>
六、小结
下篇主要讲了:
- 预处理语句:不只是防注入,还能提升性能。命名占位符比问号更清晰。
- 大对象 :用
PDO::PARAM_LOB绑定文件流,内存友好。kdbLOBCreate系列函数提供了更灵活的大对象操作。 - COPY命令:批量导入导出首选,百万级数据秒级完成。
- 错误处理:开启异常模式,通过错误码判断具体问题。
两篇加在一起,从基础连接到生产环境优化都有了。PHP连金仓这块,资料确实不多,很多细节都是自己试出来的。希望这两篇文章能让你少走点弯路。