首页 / 资讯中心 / 文章详情

PHP的PDO大对象(LOBs)

PHP的PDO大对象(LOBs) ★ FEATURED ARTICLE
前言大对象Large Object缩写 LOB在数据库语境里指的是那些体积明显超过普通标量的字段内容——图片、音频、PDF、压缩包、长文本。手册给出的经验阈值是大约 4 KB 以上并补充说有些数据库能轻松处理到 32 KB 才开始觉得大。很多人第一次接触 PDO 的 LOB 时会到处找PDO::lob*之类的方法结果一个都找不到。PDO 根本没有 LOB 专用 API。它的做法很克制只提供一个类型码PDO::PARAM_LOB配合PDOStatement::bindParam()或PDOStatement::bindColumn()使用。手册对这个类型码的解释是——它告诉 PDO把数据映射为流stream于是你可以用 PHP 的 Streams API 去操作它fgets()、fread()、stream_get_contents()、fpassthru()全都能用上。另一个常见误解是以为 LOB 支持在所有驱动上表现一致。事实并非如此。手册在多个方法页面上提醒过某些 PDO 方法属于驱动可选实现LOB 的处理方式正是这类差异的集中体现。所以本文会把驱动相关这件事摆在明面上讲而不是给一套万能代码了事。一、PDO 眼中的 LOB一个类型码加两条口子整件事的机制可以压缩成一句话PDO::PARAM_LOB就是请把这块数据当流处理的开关。它只有两个使用位置使用位置方向作用PDOStatement::bindParam()PHP 到数据库把一个流句柄作为参数写进去PDOStatement::bindColumn()数据库到 PHP把某个结果列绑定成一个流变量两个方法的签名如下以手册为准?php // 适用于 PHP 8.1public function PDOStatement::bindParam(string|int $param,mixed $var,int $type PDO::PARAM_STR,int $maxLength 0,mixed $driverOptions null): boolpublic function PDOStatement::bindColumn(string|int $column,mixed $var,int $type PDO::PARAM_STR,int $maxLength 0,mixed $driverOptions null): bool$var是引用传递这一点对 LOB 特别关键写方向上传一个fopen()返回的流资源读方向上 PDO 会把流资源塞进这个变量你必须靠引用才能拿到它。用bindValue()是拿不到读方向的流的因为bindValue()的$value是值传递。二、把文件写进数据库下面的写法对应手册里插入图片的示例思路改成了可以直接跑的完整脚本。?php // 适用于 PHP 8.0数据库为 MySQL表字段类型 LONGBLOB / MEDIUMBLOBdeclare(strict_types1);$pdo new PDO(mysql:host127.0.0.1;dbnamefiles;charsetutf8mb4,app,secret,[PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION,PDO::ATTR_EMULATE_PREPARES false,]);$path __DIR__ . /sample.png;if (!is_readable($path)) {exit(文件不可读: {$path}\n);}$fp fopen($path, rb);if ($fp false) {exit(打开文件失败\n);}$stmt $pdo-prepare(INSERT INTO images (id, content_type, image_data) VALUES (?, ?, ?));$id 1001;$mime image/png;$stmt-bindParam(1, $id, PDO::PARAM_INT);$stmt-bindParam(2, $mime, PDO::PARAM_STR);$stmt-bindParam(3, $fp, PDO::PARAM_LOB); // 关键传流句柄不是文件内容try {$pdo-beginTransaction();$stmt-execute();$pdo-commit();echo 写入完成影响行数: {$stmt-rowCount()}\n;} catch (Throwable $e) {if ($pdo-inTransaction()) {$pdo-rollBack();}throw $e;} finally {fclose($fp);}这段代码里有几处必须留意的点。不要用file_get_contents()再传给PDO::PARAM_STR。那样做会把整个文件读进 PHP 内存几 MB 的图片乘以并发数很容易顶到memory_limit。用PDO::PARAM_LOB 流句柄的意义就在于 PDO 可以按块把数据推给驱动不必在 PHP 侧整块持有。打开文件的模式用rb而不是r。Windows 上文本模式会做 CRLF 转换二进制文件会被悄悄改坏。为什么外层要包事务手册的 Oracle 示例里专门写了一句Oracle 要求在事务里做 LOB 插入否则新写入的 LOB 会在语句执行时的隐式提交中被以零长度落库。MySQL 没有这个强制要求但把 LOB 写入包进事务是现代码的好习惯——大字段写入失败的代价高回滚比留下半截数据划算。bindParam()传的是引用的流句柄。参数绑定发生在execute()真正执行的时刻所以bindParam(3, $fp, PDO::PARAM_LOB)里$fp必须是一个已打开的、指针位于起点的流。手册明确说了 PDO 会尽力用最高效的方式把文件内容送到数据库。三、把 LOB 读出来读方向的关键是bindColumn()加PDO::PARAM_LOB。手册示例直接用fpassthru()把流吐给浏览器本地文件场景换成stream_get_contents()更直观。?php // 适用于 PHP 8.0数据库为 MySQLdeclare(strict_types1);$pdo new PDO(mysql:host127.0.0.1;dbnamefiles;charsetutf8mb4,app,secret,[PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION]);$stmt $pdo-prepare(SELECT content_type, image_data FROM images WHERE id ?);$stmt-execute([1001]);// 第 4 个参数是 maxLength只对字符串类型有意义这里给 LOB 传 0 表示不限制$stmt-bindColumn(1, $mime, PDO::PARAM_STR, 255);$stmt-bindColumn(2, $blob, PDO::PARAM_LOB);if ($stmt-fetch(PDO::FETCH_BOUND)) {// 此时 $blob 是一个流资源$bytes stream_get_contents($blob);fclose($blob);$out __DIR__ . /restored.bin;file_put_contents($out, $bytes);printf(MIME%s 大小%d 字节 已写入 %s\n, $mime, strlen($bytes), $out);// 校验一致性与源文件比对哈希printf(源文件 md5: %s\n, md5_file(__DIR__ . /sample.png));printf(落盘 md5: %s\n, md5_file($out));}几个要点PDO::FETCH_BOUND表示把结果绑定到已绑定的 PHP 变量并返回true而不是返回数组。用fetch()取 LOB 时必须用这个模式。$blob拿到的是流资源用完记得fclose()否则大量读取时会堆积文件描述符。如果只想知道大小而不想载入内存可以先读流的开头或直接查LENGTH(image_data)这种数据库函数避免把几十 MB 拉进 PHP。想直接输出给浏览器例如做图片接口用fpassthru($blob)比stream_get_contents()更省内存因为它边读边写不会在 PHP 里攒一个完整副本。四、驱动差异与容量约束这是本文必须要讲清楚的一节因为很多抄来就能用的教程在这里翻车。第一手册明确提醒过部分 PDO 方法是驱动可选实现的。PDOStatement::getColumnMeta()的页面上就写着某些驱动可能不实现它。LOB 的处理方式同样属于驱动各自为政的地带不同类型映射、不同流行为、不同长度限制。所以本文的代码请务必在你的目标驱动上加一次实测不要假设换个 DSN 就照样跑。第二MySQL 里没有独立的 LOB 类型。所谓大对象落到表结构上就是BLOB/MEDIUMBLOB/LONGBLOB或者TEXT家族。这意味着两点后果单个字段能存多大受 MySQL 的max_allowed_packet限制超过就是Packet too large跟 PHP 的memory_limit是两码事要分别排查。二进制列用LONGBLOB文本列用LONGTEXT并确认字符集是utf8mb4把二进制塞进TEXT列迟早遇到编码损坏。第三PDO::PARAM_STR_NATL和PDO::PARAM_STR_CHAR与 LOB 无关。这两个是 PHP 7.2 起提供的字符串子类型标志必须与PDO::PARAM_STR按位或使用作用是给字符串字面量加N前缀。手册特别提醒对 MySQL 而言加了这个前缀的字符串会被当成utf8而不是utf8mb4解释在utf8mb4库上反而会引发排序规则冲突。既然容易惹麻烦处理 LOB 时就不要去碰它们。第四存储大字段本身是设计取舍不是技术问题。把图片放进数据库能获得事务一致性和统一备份代价是数据库体积膨胀、备份变慢、走网络传输时多一层开销。文件放磁盘、数据库只存路径也是一种正当方案选哪个取决于一致性要求和运维能力没有绝对答案。常见坑点用file_get_contents()读大文件再绑成字符串❌$stmt-bindValue(3, file_get_contents($path), PDO::PARAM_STR);✅$fp fopen($path, rb); $stmt-bindParam(3, $fp, PDO::PARAM_LOB);读 LOB 时用bindValue()或普通fetch()❌$row $stmt-fetch(PDO::FETCH_ASSOC); $stream $row[image_data];✅$stmt-bindColumn(2, $blob, PDO::PARAM_LOB); $stmt-fetch(PDO::FETCH_BOUND);忘了关闭流❌$blob ...; /* 循环里反复读从不 fclose */✅stream_get_contents($blob); fclose($blob);或读完后立刻释放Windows 上用文本模式打开二进制文件❌fopen($path, r)✅fopen($path, rb)避免 CRLF 转换破坏二进制内容把 LOB 写入排除在事务之外❌ 直接execute()后就走人 ✅beginTransaction()包住写入失败时rollBack()以为调大memory_limit就能存更大的文件❌ 遇到 Packet too large 去改memory_limit✅ MySQL 侧要调max_allowed_packetPHP 侧只需保证用流而不是整块字符串二进制字段选了 TEXT 列❌image_data TEXT✅image_data LONGBLOB文本且超长时用LONGTEXT并统一utf8mb4在 MySQL 上照抄 Oracle 的EMPTY_BLOB() RETURNING写法❌ 把INSERT ... RETURNING x INTO ?用在 MySQL 上 ✅ 那是 Oracle 专用语法MySQL 直接INSERT加PDO::PARAM_LOB即可总结项目说明核心机制PDO::PARAM_LOB把数据映射为 PHP 流写入用法bindParam($n, $fp, PDO::PARAM_LOB)$fp来自fopen($path, rb)读取用法bindColumn($n, $blob, PDO::PARAM_LOB)fetch(PDO::FETCH_BOUND)取内容stream_get_contents()或fpassthru()内存策略全程走流避免把整个字段读进 PHP 字符串驱动差异各驱动对 LOB 支持程度不同必须以目标驱动实测为准容量约束MySQL 侧看max_allowed_packet与memory_limit无关PDO 的 LOB 支持朴素得有点反直觉没有专用类、没有专用方法只有一个类型码和两条绑定口子。但正是这种设计让它天然站在流这一侧——文件不被整块读进内存读出来也是流可以直接fpassthru()转发。掌握bindParam/bindColumnPDO::PARAM_LOB这一对组合再记住驱动差异和MySQL 没有真 LOB 类型这两条前提大字段的存取就不会再是黑箱了。
阅读完成 · 觉得有帮助?
咨询建站