MongoDB CRUD 操作
概述
MongoDB 的 CRUD(创建、读取、更新、删除)操作与关系型数据库有显著不同。MongoDB 使用 BSON 文档存储数据,支持丰富的查询操作符和灵活的更新操作。
核心方法
| 操作 | 方法 | 说明 |
|---|---|---|
| 创建 | insertOne / insertMany | 插入单个/多个文档 |
| 读取 | findOne / find | 查询单个/多个文档 |
| 更新 | updateOne / updateMany / findOneAndUpdate | 原子更新 |
| 删除 | deleteOne / deleteMany / findOneAndDelete | 原子删除 |
基础概念
查询操作符
| 操作符 | 类型 | 说明 | 示例 |
|---|---|---|---|
$eq | 比较 | 等于 | ['age' => ['$eq' => 25]] |
$ne | 比较 | 不等于 | ['status' => ['$ne' => 'deleted']] |
$gt / $gte | 比较 | 大于/大于等于 | ['price' => ['$gte' => 100]] |
$lt / $lte | 比较 | 小于/小于等于 | ['age' => ['$lt' => 18]] |
$in | 比较 | 在数组中 | ['status' => ['$in' => ['active', 'pending']]] |
$nin | 比较 | 不在数组中 | ['role' => ['$nin' => ['guest']]] |
$and | 逻辑 | 与 | ['$and' => [['a' => 1], ['b' => 2]]] |
$or | 逻辑 | 或 | ['$or' => [['a' => 1], ['b' => 2]]] |
$exists | 元素 | 字段存在 | ['email' => ['$exists' => true]] |
$regex | 正则 | 正则匹配 | ['name' => ['$regex' => '^张']] |
$text | 文本 | 全文搜索 | ['$text' => ['$search' => '关键词']] |
语法与代码
插入文档
php
<?php
declare(strict_types=1);
use MongoDB\Client;
use MongoDB\BSON\ObjectId;
use MongoDB\BSON\UTCDateTime;
$client = new Client('mongodb://localhost:27017');
$collection = $client->selectCollection('testdb', 'users');
// insertOne — 插入单个文档
$result = $collection->insertOne([
'name' => '张三',
'age' => 28,
'email' => 'zhangsan@example.com',
'status' => 'active',
'roles' => ['user', 'editor'],
'createdAt' => new UTCDateTime(),
]);
$insertedId = $result->getInsertedId(); // ObjectId 对象
echo "插入ID: " . (string) $insertedId . "\n";
// insertMany — 批量插入
$result = $collection->insertMany([
[
'name' => '李四',
'age' => 32,
'email' => 'lisi@example.com',
'roles' => ['user'],
],
[
'name' => '王五',
'age' => 25,
'email' => 'wangwu@example.com',
'roles' => ['admin'],
],
]);
echo "插入数: " . $result->getInsertedCount() . "\n";查询文档
php
<?php
declare(strict_types=1);
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$collection = $client->selectCollection('testdb', 'users');
// findOne — 查询单个文档
$user = $collection->findOne(['name' => '张三']);
if ($user) {
echo "ID: " . $user['_id'] . "\n";
echo "名称: " . $user['name'] . "\n";
echo "角色: " . implode(', ', $user['roles']) . "\n";
}
// 按 ObjectId 查询
$user = $collection->findOne(['_id' => new ObjectId('65a1b2c3d4e5f6a7b8c9d0e1')]);
// find — 查询多个文档
$cursor = $collection->find([
'age' => ['$gte' => 25, '$lte' => 35],
'status' => 'active',
]);
foreach ($cursor as $doc) {
echo "{$doc['name']} - {$doc['age']}岁\n";
}
// find 带选项
$cursor = $collection->find(
['status' => 'active'],
[
'sort' => ['age' => -1], // 按年龄降序
'limit' => 10, // 最多10条
'skip' => 0, // 跳过0条(分页偏移)
'projection' => ['name' => 1, 'age' => 1, '_id' => 0], // 字段投影
]
);
// 转为数组
$users = $collection->find(['status' => 'active'])->toArray();
// 计数
$count = $collection->countDocuments(['status' => 'active']);
// 判断存在
$exists = $collection->findOne(['email' => 'zhangsan@example.com']) !== null;更新文档
php
<?php
declare(strict_types=1);
use MongoDB\Client;
use MongoDB\BSON\UTCDateTime;
$client = new Client('mongodb://localhost:27017');
$collection = $client->selectCollection('testdb', 'users');
// updateOne — 更新第一个匹配的文档
$result = $collection->updateOne(
['name' => '张三'],
['$set' => ['age' => 29, 'updatedAt' => new UTCDateTime()]]
);
echo "匹配: {$result->getMatchedCount()}, 修改: {$result->getModifiedCount()}\n";
// updateMany — 更新所有匹配的文档
$result = $collection->updateMany(
['status' => 'active'],
['$set' => ['updatedAt' => new UTCDateTime()]]
);
// 更新操作符
// $set — 设置字段值
// $unset — 删除字段
// $inc — 原子递增
// $push — 向数组追加
// $pull — 从数组移除
// $addToSet — 向数组添加(去重)
// $rename — 重命名字段
// 原子递增
$collection->updateOne(
['name' => '张三'],
['$inc' => ['loginCount' => 1, 'score' => 5]]
);
// 数组操作
$collection->updateOne(
['name' => '张三'],
['$push' => ['roles' => 'admin']]
);
$collection->updateOne(
['name' => '张三'],
['$addToSet' => ['tags' => 'vip']] // 不会重复添加
);
$collection->updateOne(
['name' => '张三'],
['$pull' => ['roles' => 'guest']]
);
// findOneAndUpdate — 原子查找并更新
$updated = $collection->findOneAndUpdate(
['name' => '张三'],
['$set' => ['status' => 'inactive']],
['returnDocument' => MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_AFTER]
);删除文档
php
<?php
declare(strict_types=1);
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$collection = $client->selectCollection('testdb', 'users');
// deleteOne — 删除第一个匹配的文档
$result = $collection->deleteOne(['name' => '张三']);
echo "删除数: " . $result->getDeletedCount() . "\n";
// deleteMany — 删除所有匹配的文档
$result = $collection->deleteMany(['status' => 'deleted']);
echo "删除数: " . $result->getDeletedCount() . "\n";
// findOneAndDelete — 原子查找并删除
$deleted = $collection->findOneAndDelete(
['name' => '张三']
);
if ($deleted) {
echo "已删除: " . $deleted['name'] . "\n";
}
// 删除集合中所有文档(保留索引)
$collection->deleteMany([]);实战示例
通用 Repository 模式
php
<?php
declare(strict_types=1);
use MongoDB\Client;
use MongoDB\BSON\ObjectId;
use MongoDB\BSON\UTCDateTime;
abstract class BaseRepository
{
protected MongoDB\Driver\Collection $collection;
public function __construct(protected Client $client, protected string $dbName, protected string $collectionName)
{
$this->collection = $client->selectCollection($dbName, $collectionName);
}
public function findById(string $id): ?array
{
$doc = $this->collection->findOne(['_id' => new ObjectId($id)]);
return $doc ? $this->toArray($doc) : null;
}
public function findOne(array $filter): ?array
{
$doc = $this->collection->findOne($filter);
return $doc ? $this->toArray($doc) : null;
}
public function find(array $filter = [], array $options = []): array
{
$cursor = $this->collection->find($filter, $options);
return array_map(fn($doc) => $this->toArray($doc), iterator_to_array($cursor));
}
public function paginate(array $filter, int $page = 1, int $perPage = 20): array
{
$total = $this->collection->countDocuments($filter);
$docs = $this->find($filter, [
'skip' => ($page - 1) * $perPage,
'limit' => $perPage,
'sort' => ['createdAt' => -1],
]);
return [
'data' => $docs,
'total' => $total,
'page' => $page,
'perPage' => $perPage,
'totalPages' => (int) ceil($total / $perPage),
];
}
public function insert(array $data): string
{
$data['createdAt'] = new UTCDateTime();
$data['updatedAt'] = new UTCDateTime();
$result = $this->collection->insertOne($data);
return (string) $result->getInsertedId();
}
public function update(string $id, array $data): bool
{
$data['updatedAt'] = new UTCDateTime();
unset($data['_id']);
$result = $this->collection->updateOne(
['_id' => new ObjectId($id)],
['$set' => $data]
);
return $result->getModifiedCount() > 0;
}
public function delete(string $id): bool
{
$result = $this->collection->deleteOne(['_id' => new ObjectId($id)]);
return $result->getDeletedCount() > 0;
}
public function count(array $filter = []): int
{
return $this->collection->countDocuments($filter);
}
protected function toArray(object $doc): array
{
$data = (array) $doc;
if (isset($data['_id']) && $data['_id'] instanceof ObjectId) {
$data['id'] = (string) $data['_id'];
}
return $data;
}
}
// 使用示例
class UserRepository extends BaseRepository
{
public function __construct(Client $client)
{
parent::__construct($client, 'app_db', 'users');
}
public function findActiveUsers(int $minAge = 0): array
{
return $this->find([
'status' => 'active',
'age' => ['$gte' => $minAge],
], ['sort' => ['createdAt' => -1]]);
}
public function findByEmail(string $email): ?array
{
return $this->findOne(['email' => $email]);
}
public function addRole(string $userId, string $role): bool
{
$result = $this->collection->updateOne(
['_id' => new ObjectId($userId)],
['$addToSet' => ['roles' => $role]]
);
return $result->getModifiedCount() > 0;
}
}注意事项
原子操作与并发
php
<?php
// MongoDB 的 findAndModify 系列方法是原子的
// 适合实现: 抢占式更新、唯一计数器、队列消费
// 1. 原子计数器
$collection->findOneAndUpdate(
['_id' => 'order_counter'],
['$inc' => ['value' => 1]],
['upsert' => true, 'returnDocument' => 2] // RETURN_DOCUMENT_AFTER
);
// 2. 队列消费(安全弹出)
$task = $collection->findOneAndUpdate(
['status' => 'pending'],
['$set' => ['status' => 'processing', 'startedAt' => new UTCDateTime()]],
['sort' => ['priority' => -1], 'returnDocument' => 2]
);
// 3. 乐观锁(版本控制)
$collection->updateOne(
['_id' => new ObjectId($id), 'version' => 3],
['$set' => ['field' => 'value', 'version' => 4]]
);字段投影
php
<?php
// 投影可以减少网络传输量
$cursor = $collection->find([], [
'projection' => [
'name' => 1, // 包含
'email' => 1, // 包含
'password' => 0, // 排除(敏感字段)
'_id' => 0, // 排除默认返回的 _id
],
]);
// 注意: 不能同时指定 include 和 exclude(_id 除外)最佳实践
1. 批量操作优化
php
<?php
// 批量插入 — 使用 insertMany 而非循环 insertOne
$documents = [];
for ($i = 0; $i < 1000; $i++) {
$documents[] = ['index' => $i, 'data' => "item_{$i}"];
}
$collection->insertMany($documents);
// 批量更新 — 使用 BulkWrite
$bulk = new MongoDB\Driver\BulkWrite(['ordered' => false]);
foreach ($updates as $update) {
$bulk->update(
['_id' => new ObjectId($update['id'])],
['$set' => $update['data']]
);
}
$manager->executeBulkWrite('app_db.users', $bulk);2. 索引利用
php
<?php
// 确保查询字段有索引
// 创建索引(只需执行一次)
$collection->createIndex(['email' => 1], ['unique' => true]);
$collection->createIndex(['status' => 1, 'createdAt' => -1]); // 复合索引
$collection->createIndex(['name' => 'text']); // 文本索引
// 使用 explain 分析查询
$collection->find(['email' => 'test@example.com'])->explain();