PDO连金仓数据库(下篇):预处理语句、大对象和批量操作

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();

bindParambindValue的区别: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', ',');
?>

六、小结

下篇主要讲了:

  1. 预处理语句:不只是防注入,还能提升性能。命名占位符比问号更清晰。
  2. 大对象 :用PDO::PARAM_LOB绑定文件流,内存友好。kdbLOBCreate系列函数提供了更灵活的大对象操作。
  3. COPY命令:批量导入导出首选,百万级数据秒级完成。
  4. 错误处理:开启异常模式,通过错误码判断具体问题。

两篇加在一起,从基础连接到生产环境优化都有了。PHP连金仓这块,资料确实不多,很多细节都是自己试出来的。希望这两篇文章能让你少走点弯路。

相关推荐
袋鱼不重32 分钟前
我的神奇同事,AI 用多了居然写了个 Open In Codex
前端·后端·ai编程
用户83562907805135 分钟前
使用 Python 操作 Word 内容控件
后端·python
像我这样帅的人丶你还36 分钟前
啥? 前端也要会干Java?🛵🛵🛵
后端
Hommy8838 分钟前
【剪映小助手】添加贴纸接口(Add Sticker)
后端·github·剪映小助手·视频剪辑自动化·剪映api
CaffeinePro1 小时前
FastAPI响应处理:返回值、状态码、响应头与异常标准化与案例解析
后端
HuanYu1 小时前
PageHelper分页的原理
后端
于先生吖2 小时前
SpringBoot对接大模型开发AI命理测算系统:八字排盘与AI解析接口源码全解
人工智能·spring boot·后端
张不才2 小时前
一个静默吞数据的时间戳陷阱
后端
李少兄2 小时前
从原理到实战:Spring IoC/DI 核心知识体系与高频面试题全解
java·后端·spring
ServBay2 小时前
ServBay 1.30.0 更新:双平台引入 MCP 服务,AI 编程助手成为全栈本地运维
后端·ai编程