Skip to content

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();

参考链接