將二進制圖片直接存入 MySQL(BLOB/MEDIUMBLOB/LONGBLOB)雖然技術可行,但存在明顯缺點:
最佳實踐:圖片存文件系統/對象存儲(OSS/S3),數據庫僅存路徑/URL/元數據。本文僅為技術探討與特殊場景(如強合規要求、小圖標嵌入)提供方案。
直接拼接二進制字符串進 SQL 會導致語法錯誤(含單引號、NULL、控制字符等)。需對數據進行「編碼/轉義」後再插入,讀取時「解碼」。常見方案對比:
| 方案 | 原理 | 存儲體積 | 讀取處理 | 優點 | 缺點 |
|---|---|---|---|---|---|
| addslashes() | 轉義單引號、雙引號、反斜線、NULL | 原大小 | 直接輸出 | 代碼簡單 | 不完整,某些字節仍導致語法錯誤 |
| base64_encode() | 二進制 → ASCII 字符(A-Z,a-z,0-9,+,/) | ~133% 原大小 | base64_decode() | 純文本安全,無需轉義 | 體積增大 33%,CPU 開銷大 |
| bin2hex() | 二進制 → 十六進制字符串 (0-9,a-f) | 200% 原大小 | UNHEX() 或 hex2bin() | MySQL 可直接用 0x 前綴插入,無需轉義 | 體積翻倍,存儲/傳輸成本高 |
| PMA_sqlAddslashes() | phpMyAdmin 內部函數,完整轉義 | 原大小 | 直接輸出 | 體積不變,兼容性好 | 需複製函數代碼,維護成本 |
| PDO 參數綁定 | 預處理語句,二進制參數單獨傳輸 | 原大小 | 直接輸出 | 最安全、最高效、體積不變 | 需 PDO 支援,舊 mysql_* 不適用 |
<?php
$pdo = new PDO("mysql:host=127.0.0.1;dbname=test;charset=utf8mb4", "root", "pass", [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
// 插入
$imgPath = "photo.jpg";
$imgData = file_get_contents($imgPath); // 獲取二進制內容
$stmt = $pdo->prepare("INSERT INTO `images` (`name`, `type`, `data`) VALUES (?, ?, ?)");
$stmt->bindParam(1, $imgPath, PDO::PARAM_STR);
$stmt->bindParam(2, mime_content_type($imgPath), PDO::PARAM_STR);
$stmt->bindParam(3, $imgData, PDO::PARAM_LOB); // 關鍵:PARAM_LOB 處理大對象
$stmt->execute();
echo "插入成功,ID: " . $pdo->lastInsertId();
// 讀取並輸出
$stmt = $pdo->prepare("SELECT `type`, `data` FROM `images` WHERE `id` = ?");
$stmt->execute([1]);
$stmt->bindColumn(1, $type);
$stmt->bindColumn(2, $data, PDO::PARAM_LOB);
$stmt->fetch(PDO::FETCH_BOUND);
header("Content-Type: $type");
echo $data;
?>
<?php
$mysqli = new mysqli("127.0.0.1", "root", "pass", "test");
$imgData = file_get_contents("photo.jpg");
$hexData = bin2hex($imgData); // 二進制轉十六進制字符串
$sql = "INSERT INTO `images` (`data`) VALUES (0x$hexData)"; // 0x 前綴告訴 MySQL 這是十六進制
$mysqli->query($sql);
// 讀取
$res = $mysqli->query("SELECT `data` FROM `images` WHERE `id` = 1");
$row = $res->fetch_assoc();
header("Content-Type: image/jpeg");
echo hex2bin($row['data']); // PHP 5.4+ hex2bin()
?>
若必須用 mysqli/mysql 老擴展且不想引入 PDO,可直接移植 phpMyAdmin 的轉義函數:
function PMA_sqlAddslashes($str, $is_like = false, $crlf = false, $php_code = false) {
if ($is_like) {
$str = str_replace('\\', '\\\\', $str);
} else {
$str = str_replace('\\', '\\\\', $str); // 先轉義反斜線
}
if ($crlf) {
$str = str_replace("\n", '\\n', $str);
$str = str_replace("\r", '\\r', $str);
$str = str_replace("\t", '\\t', $str);
}
if ($php_code) {
$str = str_replace("'", "\\'", $str);
} else {
$str = str_replace("'", "''", $str); // SQL 標準:單引號轉兩個單引號
}
return $str;
}
// 使用
$imgData = file_get_contents("photo.jpg");
$safeData = PMA_sqlAddslashes($imgData);
$mysqli->query("INSERT INTO `images` (`data`) VALUES ('$safeData')");
<form method="post" action="upload.php" enctype="multipart/form-data">
<input type="hidden" name="MAX_FILE_SIZE" value="2097152"> <!-- 2MB -->
<input type="file" name="img" accept="image/*" required>
<button type="submit">上傳存入資料庫</button>
</form>
<?php
$pdo = new PDO("mysql:host=127.0.0.1;dbname=test;charset=utf8mb4", "root", "pass", [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['img'])) {
$file = $_FILES['img'];
if ($file['error'] !== UPLOAD_ERR_OK) die("上傳失敗:錯誤碼 {$file['error']}");
if ($file['size'] > 2 * 1024 * 1024) die("文件超過 2MB 限制");
$mime = mime_content_type($file['tmp_name']);
if (!in_array($mime, ['image/jpeg', 'image/png', 'image/gif', 'image/webp'])) die("僅支援 JPG/PNG/GIF/WebP");
$data = file_get_contents($file['tmp_name']);
$stmt = $pdo->prepare("INSERT INTO `images` (`name`, `type`, `data`, `size`) VALUES (?, ?, ?, ?)");
$stmt->execute([$file['name'], $mime, $data, $file['size']]);
echo "存入成功,ID: " . $pdo->lastInsertId() . " <a href='view.php?id=" . $pdo->lastInsertId() . "'>查看</a>";
}
?>
<?php
$pdo = new PDO("mysql:host=127.0.0.1;dbname=test;charset=utf8mb4", "root", "pass", [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
$id = (int)($_GET['id'] ?? 0);
$stmt = $pdo->prepare("SELECT `type`, `data` FROM `images` WHERE `id` = ?");
$stmt->execute([$id]);
$stmt->bindColumn(1, $type);
$stmt->bindColumn(2, $data, PDO::PARAM_LOB);
if ($stmt->fetch(PDO::FETCH_BOUND)) {
header("Content-Type: $type");
header("Content-Length: " . strlen($data));
echo $data;
} else {
http_response_code(404);
echo "圖片不存在";
}
?>
| 方案 | 數據庫實際佔用 | 備註 |
|---|---|---|
| PDO PARAM_LOB | 17.0 KB | 原始大小,最高效 |
| PMA_sqlAddslashes | 17.0 KB | 原始大小,兼容老擴展 |
| base64_encode | 22.7 KB | +33% |
| bin2hex + 0x | 34.0 KB | +100% |