在 PDO MySQL 中读取 MEDIUMBLOB 时大约需要 1 MB max_allowed_packet
Working around 1 MB max_allowed_packet when reading MEDIUMBLOB in PDO MySQL
我正在使用 PDO 检索 MySQL 数据库中 table 的 MEDIUMBLOB
列中的 5 MB 值。 MEDIUMBLOB
最多可存储 16 MB,但由于 max_allowed_packet
,PDO 将其截断为 1 MB。我尝试了 Large Objects, but PDO's MySQL driver produces a string, not a stream (bug 40913, reported as "still present in PHP-5.6.5"). In effect, it treats PDO::PARAM_LOB
as a synonym for PDO::PARAM_STR
. Answers to BLOB Download Truncated at 1 MB... 中提到的 bindColumn
建议在服务器上的 my.cnf
中增加 max_allowed_packet
变量,但我没有权限更改 my.cnf
,这可能会影响服务器的其他用户。我知道可以解决这个问题,因为 phpMyAdmin 可以从同一台服务器下载如此大的 BLOB
。
table是这样定义的:
CREATE TABLE IF NOT EXISTS cache_items (
`cache_id` INTEGER UNSIGNED PRIMARY KEY AUTO_INCREMENT,
`name` VARBINARY(63),
`value` MEDIUMBLOB NOT NULL,
`created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`expires` DATETIME NOT NULL,
UNIQUE (`name`),
INDEX (`expires`)
) ENGINE=INNODB;
PHP代码:
<?php
require_once("dbsettings.php");
$db = new PDO($pdo_dsn, $pdo_username, $pdo_password, $pdo_options);
$name = 'hello';
$read_stmt = $db->prepare("
SELECT `value` FROM `cache_items`
WHERE `name` = :n AND `expires` > CURRENT_TIMESTAMP
ORDER BY `cache_id` DESC LIMIT 1
");
$read_stmt->execute([':n' => $name]);
$read_stmt->bindColumn(1, $value_fp, PDO::PARAM_LOB);
$ok = $read_stmt->fetch(PDO::FETCH_BOUND);
echo gettype($bodyfp); // string, not resource, because of bug 40913
echo strlen($bodyfp); // 1048576, not 5xxxxxx, because of max_allowed_packet
那么一个程序应该如何检索一个大的 BLOB
?或者将每个 value
存储在一个目录中的文件中,然后执行一个周期性任务,从目录中删除与 cache_items
中未过期条目不对应的任何文件会更实际吗?
我通过制作一个循环来解决这个问题,该循环使用 MySQL 的 SUBSTRING
函数在循环中读取更小的值块,其中每个块小于一个数据包。
<?php
// [connection setup omitted]
$stat_stmt = $db->prepare("
SELECT `cache_id`, LENGTH(`value`) FROM `cache_items`
WHERE `name` = :n AND `expires` > CURRENT_TIMESTAMP
ORDER BY `cache_id` DESC LIMIT 1
");
$read_stmt = $db->prepare("
SELECT SUBSTRING(`value` FROM :start + 1 FOR 250000)
FROM `cache_items`
WHERE `cache_id` = :id
");
// Find the ID and length of the cache entry
$stat_stmt->execute($stat_stmt, [':n'=>$name]);
$files = $stat_stmt->fetchAll(PDO::FETCH_NUM);
if (!$files) {
exit;
}
list($cache_id, $length) = $files[0];
// Read in a loop to work around servers with small MySQL max_allowed_packet
// as well as the fact that PDO::PARAM_LOB on MySQL produces a string instead
// of a stream
// https://bugs.php.net/bug.php?id=40913
$length_so_far = 0;
$body = [];
while ($length_so_far < $length) {
$read_stmt->execute([':id'=>$cache_id, ':start'=>$length_so_far]);
$piece = $read_stmt->fetchAll(PDO::FETCH_COLUMN, 0);
if (!$piece) {
exit;
}
$piece = $piece[0];
if (strlen($piece) < 1) {
exit;
}
$length_so_far += strlen($piece);
$body[] = $piece;
}
echo implode('', $body);
我正在使用 PDO 检索 MySQL 数据库中 table 的 MEDIUMBLOB
列中的 5 MB 值。 MEDIUMBLOB
最多可存储 16 MB,但由于 max_allowed_packet
,PDO 将其截断为 1 MB。我尝试了 Large Objects, but PDO's MySQL driver produces a string, not a stream (bug 40913, reported as "still present in PHP-5.6.5"). In effect, it treats PDO::PARAM_LOB
as a synonym for PDO::PARAM_STR
. Answers to BLOB Download Truncated at 1 MB... 中提到的 bindColumn
建议在服务器上的 my.cnf
中增加 max_allowed_packet
变量,但我没有权限更改 my.cnf
,这可能会影响服务器的其他用户。我知道可以解决这个问题,因为 phpMyAdmin 可以从同一台服务器下载如此大的 BLOB
。
table是这样定义的:
CREATE TABLE IF NOT EXISTS cache_items (
`cache_id` INTEGER UNSIGNED PRIMARY KEY AUTO_INCREMENT,
`name` VARBINARY(63),
`value` MEDIUMBLOB NOT NULL,
`created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`expires` DATETIME NOT NULL,
UNIQUE (`name`),
INDEX (`expires`)
) ENGINE=INNODB;
PHP代码:
<?php
require_once("dbsettings.php");
$db = new PDO($pdo_dsn, $pdo_username, $pdo_password, $pdo_options);
$name = 'hello';
$read_stmt = $db->prepare("
SELECT `value` FROM `cache_items`
WHERE `name` = :n AND `expires` > CURRENT_TIMESTAMP
ORDER BY `cache_id` DESC LIMIT 1
");
$read_stmt->execute([':n' => $name]);
$read_stmt->bindColumn(1, $value_fp, PDO::PARAM_LOB);
$ok = $read_stmt->fetch(PDO::FETCH_BOUND);
echo gettype($bodyfp); // string, not resource, because of bug 40913
echo strlen($bodyfp); // 1048576, not 5xxxxxx, because of max_allowed_packet
那么一个程序应该如何检索一个大的 BLOB
?或者将每个 value
存储在一个目录中的文件中,然后执行一个周期性任务,从目录中删除与 cache_items
中未过期条目不对应的任何文件会更实际吗?
我通过制作一个循环来解决这个问题,该循环使用 MySQL 的 SUBSTRING
函数在循环中读取更小的值块,其中每个块小于一个数据包。
<?php
// [connection setup omitted]
$stat_stmt = $db->prepare("
SELECT `cache_id`, LENGTH(`value`) FROM `cache_items`
WHERE `name` = :n AND `expires` > CURRENT_TIMESTAMP
ORDER BY `cache_id` DESC LIMIT 1
");
$read_stmt = $db->prepare("
SELECT SUBSTRING(`value` FROM :start + 1 FOR 250000)
FROM `cache_items`
WHERE `cache_id` = :id
");
// Find the ID and length of the cache entry
$stat_stmt->execute($stat_stmt, [':n'=>$name]);
$files = $stat_stmt->fetchAll(PDO::FETCH_NUM);
if (!$files) {
exit;
}
list($cache_id, $length) = $files[0];
// Read in a loop to work around servers with small MySQL max_allowed_packet
// as well as the fact that PDO::PARAM_LOB on MySQL produces a string instead
// of a stream
// https://bugs.php.net/bug.php?id=40913
$length_so_far = 0;
$body = [];
while ($length_so_far < $length) {
$read_stmt->execute([':id'=>$cache_id, ':start'=>$length_so_far]);
$piece = $read_stmt->fetchAll(PDO::FETCH_COLUMN, 0);
if (!$piece) {
exit;
}
$piece = $piece[0];
if (strlen($piece) < 1) {
exit;
}
$length_so_far += strlen($piece);
$body[] = $piece;
}
echo implode('', $body);