MongoDB 以 JSON-like 文檔模型、動態 Schema、水平擴展能力,成為現代應用(日誌、IoT、內容管理、推薦系統)的首選 NoSQL。PHP 官方驅動 mongodb (Composer 包 mongodb/mongodb) 提供了優雅的 ODM 風格 API。本文涵蓋環境建置、CRUD、索引、聚合、事務、最佳實務。
# 1. 安裝 PHP 擴展 (需先裝 libmongoc, libbson)
pecl install mongodb
# 2. 啟用擴展
echo "extension=mongodb.so" > /etc/php.d/50-mongodb.ini
# 3. 安裝官方庫 (提供高階 API)
composer require mongodb/mongodb
# 4. 驗證
php -r "echo extension_loaded('mongodb') ? 'OK' : 'FAIL';"
<?php
require 'vendor/autoload.php';
$client = new MongoDB\Client(
'mongodb://user:pass@host1:27017,host2:27017/?replicaSet=rs0&authSource=admin',
[],
[
'typeMap' => [
'root' => 'array', // 結果預設回傳陣列而非物件
'document' => 'array',
'array' => 'array',
],
'driverOptions' => [
'connectTimeoutMS' => 5000,
'socketTimeoutMS' => 10000,
],
]
);
$db = $client->selectDatabase('myapp');
$coll = $db->selectCollection('users');
// 測試連線
try {
$client->listDatabases();
echo "MongoDB 連線成功\n";
} catch (MongoDB\Driver\Exception\ConnectionTimeoutException $e) {
die("連線失敗:" . $e->getMessage());
}
?>
<?php
// 單筆
$insertOneResult = $coll->insertOne([
'username' => 'alice',
'email' => 'alice@example.com',
'roles' => ['user'],
'profile' => ['age' => 28, 'city' => 'Taipei'],
'created_at' => new MongoDB\BSON\UTCDateTime(), // 當前毫秒時間戳
'status' => 1,
]);
echo "Inserted ID: {$insertOneResult->getInsertedId()}\n";
// 批次 (無序寫入,效能最佳)
$insertManyResult = $coll->insertMany([
['username' => 'bob', 'email' => 'bob@example.com', 'roles' => ['user'], 'created_at' => new MongoDB\BSON\UTCDateTime()],
['username' => 'charlie', 'email' => 'charlie@example.com', 'roles' => ['admin'], 'created_at' => new MongoDB\BSON\UTCDateTime()],
]);
echo "Inserted IDs: " . implode(', ', array_map(fn($id) => (string)$id, $insertManyResult->getInsertedIds())) . "\n";
<?php
// 單筆
$user = $coll->findOne(['username' => 'alice']);
// 多筆 + 投影 + 排序 + 分頁
$cursor = $coll->find(
['status' => 1, 'profile.age' => ['$gte' => 18]], // 過濾條件
[
'projection' => ['username' => 1, 'email' => 1, 'profile.city' => 1], // 只取需要欄位
'sort' => ['created_at' => -1], // -1 降序
'skip' => 0,
'limit' => 20,
]
);
foreach ($cursor as $doc) {
echo "{$doc['username']} - {$doc['email']} - {$doc['profile']['city']}\n";
}
// 計數
$count = $coll->countDocuments(['status' => 1]);
echo "Active users: $count\n";
// 判斷是否存在
$exists = $coll->countDocuments(['email' => 'test@test.com'], ['limit' => 1]) > 0;
<?php
// 單筆更新 (原子操作符)
$updateResult = $coll->updateOne(
['username' => 'alice'],
[
'$set' => ['profile.age' => 29, 'last_login' => new MongoDB\BSON\UTCDateTime()],
'$inc' => ['login_count' => 1], // 計數器遞增
'$push' => ['tags' => 'vip'], // 陣列追加
'$addToSet' => ['roles' => 'premium'], // 唯一陣列追加
],
['upsert' => false] // 不存在不插入
);
echo "Matched: {$updateResult->getMatchedCount()}, Modified: {$updateResult->getModifiedCount()}\n";
// 批次更新
$coll->updateMany(
['roles' => 'user'],
['$set' => ['status' => 2]] // 批量停用
);
// 替換整份文檔 (保留 _id)
$coll->replaceOne(['_id' => $id], $newDocument);
<?php
$coll->deleteOne(['username' => 'spam_user']);
$coll->deleteMany(['status' => 0, 'created_at' => ['$lt' => new MongoDB\BSON\UTCDateTime(strtotime('-1 year'))]]);
<?php
// 單欄索引
$coll->createIndex(['username' => 1], ['unique' => true, 'name' => 'uniq_username']);
// 複合索引 (查詢順序要匹配)
$coll->createIndex(['status' => 1, 'profile.city' => 1, 'created_at' => -1], ['name' => 'idx_status_city_time']);
// 文字索引 (全文搜尋)
$coll->createIndex(['username' => 'text', 'email' => 'text'], ['name' => 'txt_search']);
// TTL 索引 (自動過期,如 Session、驗證碼)
$coll->createIndex(['expire_at' => 1], ['expireAfterSeconds' => 0, 'name' => 'ttl_expire']);
// 查看索引
foreach ($coll->listIndexes() as $index) {
echo $index->getName() . ": " . json_encode($index->getKey()) . "\n";
}
<?php
// 統計各城市用戶數,取前 10
$pipeline = [
['$match' => ['status' => 1]],
['$group' => [
'_id' => '$profile.city',
'count' => ['$sum' => 1],
'avg_age' => ['$avg' => '$profile.age'],
]],
['$sort' => ['count' => -1]],
['$limit' => 10],
['$project' => ['_id' => 0, 'city' => '$_id', 'count' => 1, 'avg_age' => 1]],
];
$cursor = $coll->aggregate($pipeline);
foreach ($cursor as $doc) {
echo "{$doc['city']}: {$doc['count']} users, avg age {$doc['avg_age']}\n";
}
<?php
$session = $client->startSession();
$session->startTransaction();
try {
$coll->insertOne(['username' => 'tx_user', 'balance' => 100], ['session' => $session]);
$coll->updateOne(['username' => 'merchant'], ['$inc' => ['balance' => -100]], ['session' => $session]);
// 模擬業務邏輯
if (rand(0, 1)) throw new Exception('模擬失敗');
$session->commitTransaction();
echo "事務提交成功\n";
} catch (Exception $e) {
$session->abortTransaction();
echo "事務回滾:" . $e->getMessage() . "\n";
} finally {
$session->endSession();
}
?>
<?php
$bucket = $db->selectGridFSBucket();
// 上傳
$stream = fopen('/path/to/large_video.mp4', 'rb');
$fileId = $bucket->uploadFromStream('large_video.mp4', $stream, [
'metadata' => ['uploader' => 'alice', 'category' => 'video'],
]);
fclose($stream);
// 下載
$bucket->downloadToStream($fileId, fopen('/path/to/save.mp4', 'wb'));
// 刪除
$bucket->delete($fileId);
| 模式 | 適用場景 | 範例 |
|---|---|---|
| 內嵌文檔 | 一對少、親子強關聯、常一起讀取 | 用戶 profile、訂單 items (少量) |
| 引用 | 一對多、多對多、獨立生命週期、需單獨查詢 | 用戶 _id → 文章 author_id |
| 混合 | 熱點資料內嵌、冷資料引用 | 文章內嵌前 3 則留言,其餘引用 comments 集合 |
| Bucket 模式 | 時間序列、IoT 感測器、日誌 | 每文檔存 1 小時資料點陣列 |
['projection' => ['_id' => 0, 'username' => 1]] 只傳需要欄位,減少網路/記憶體。explain() 顯示 "totalKeysExamined" > 0, "totalDocsExamined" == 0。insertMany/bulkWrite 替代迴圈單筆。MongoDB\Client,PHP-FPM 請求間複用 (需啟用持久連線或用 Swoole/RoadRunner)。writeConcern: {w: 'majority', j: true}、高可用讀 readPreference: 'secondaryPreferred'。db.currentOp()、db.serverStatus()、mongostat、Prometheus Exporter。| 坑點 | 後果 | 正確做法 |
|---|---|---|
| 用陣列索引當 _id | 分片失效、分布不均 | 用 ObjectId 或 Snowflake/UUID |
| 大文檔無限增長 | 16MB 限制、效能劣化 | Bucket 模式、拆集合 |
| 未建立索引就上線 | 全表掃描、CPU 飆高 | 上線前 explain() 驗證 |
| 查詢不加 limit | 記憶體爆炸、游標超時 | 永遠加 limit、游標分批 |
| 直接存 DateTime 物件 | 序列化問題、時區混亂 | 用 MongoDB\BSON\UTCDateTime (毫秒 UTC) |
| 事務用在高併發寫入 | 鎖競爭、效能崩潰 | 事務僅用於強一致必要場景 |
PHP + MongoDB 組合極適合快速迭代、Schema 多變、海量非結構化資料場景。掌握「文檔建模、索引策略、聚合管道、事務邊界、連線池管理」五大核心,即能駕馭生產環境。記住:**MongoDB 不是關係型資料庫的替代品,而是互補工具**——關係型管核心交易、強一致;MongoDB 管彈性資料、高吞吐、水平擴展。