55、MongoDB文档操作:BSON构建、insert_one/find_one/update_one/delete_one、批量操作
MongoDB 这东西,说白了就是个文档数据库。跟咱们熟悉的 MySQL 那种关系型数据库不一样,它存的是 JSON 风格的文档。嗯,准确地说,是 BSON——一种二进制的 JSON 变体。我刚开始接触的时候也觉得别扭,用惯了 SQL 的人,突然要跟 JSON 打交道,总感觉少了点约束。但用久了你会发现,这种灵活性在某些场景下简直是降维打击。
BSON 构建:MongoDB 的数据基石
BSON 是 MongoDB 内部存储数据的格式。你在 C 语言里操作 MongoDB,本质上就是在拼装和解析 BSON 对象。我个人习惯用 bson_t 结构体来构建文档,它就像一块乐高底板,你可以往上堆各种字段。
核心要点:BSON 支持的数据类型比 JSON 丰富,比如 Date、Binary、ObjectId 等。在 C 驱动里,我们通过 BCON 宏或者 bson_append_xxx 系列函数来构建。
#include <bson/bson.h>
#include <mongoc/mongoc.h>
// 方法一:使用 BCON 宏(推荐,简洁)
bson_t *doc = BCON_NEW(
"name", BCON_UTF8("张三"),
"age", BCON_INT32(28),
"city", BCON_UTF8("北京"),
"skills", "[", "C", "MongoDB", "Linux", "]"
);
// 方法二:使用 bson_append 系列函数(更灵活)
bson_t doc2;
bson_init(&doc2);
bson_append_utf8(&doc2, "name", -1, "李四", -1);
bson_append_int32(&doc2, "age", -1, 32);
bson_append_utf8(&doc2, "city", -1, "上海");
// 嵌套文档
bson_t address;
bson_init(&address);
bson_append_utf8(&address, "street", -1, "南京路", -1);
bson_append_int32(&address, "zip", -1, 200001);
bson_append_document(&doc2, "address", -1, &address);
bson_destroy(&address);
// 别忘了释放
bson_destroy(doc);
bson_destroy(&doc2);
我的经验:BCON 宏写起来快,但调试时不好看中间结果。我一般在原型阶段用 BCON,到了需要动态构建复杂查询条件时,改用 bson_append 系列函数。另外,记得每次用完都要 bson_destroy,C 语言没有垃圾回收,内存泄漏是家常便饭。
insert_one:插入第一条文档
插入操作很简单,但有个坑——MongoDB 会自动给每个文档生成一个 _id 字段。如果你自己没指定,它就用 ObjectId。我建议你显式指定 _id,尤其是在做数据迁移或批量导入时,保持主键可控很重要。
mongoc_client_t *client = mongoc_client_new("mongodb://localhost:27017");
mongoc_collection_t *collection = mongoc_client_get_collection(client, "testdb", "users");
bson_t *doc = BCON_NEW(
"_id", BCON_OID(&oid), // 自己生成 ObjectId
"name", BCON_UTF8("王五"),
"age", BCON_INT32(25)
);
bson_error_t error;
if (!mongoc_collection_insert_one(collection, doc, NULL, NULL, &error)) {
fprintf(stderr, "插入失败: %s\n", error.message);
}
bson_destroy(doc);
mongoc_collection_destroy(collection);
mongoc_client_destroy(client);
注意:插入时如果 _id 重复,会返回错误。我曾经在批量导入时忘了去重,结果跑了一半程序崩了,查了半天才发现是主键冲突。所以,批量操作前一定要检查数据唯一性。
find_one:查询单条文档
查询是重头戏。MongoDB 的查询语法跟 SQL 的 WHERE 子句有点像,但用的是 BSON 文档来表达条件。我刚开始写的时候老是把 $gt 写成 $gte,少了个 e 意思就全变了。
// 查询 age 大于 25 的用户
bson_t *query = BCON_NEW("age", "{", "$gt", BCON_INT32(25), "}");
const bson_t *doc;
mongoc_cursor_t *cursor = mongoc_collection_find_one(
collection,
query,
NULL,
NULL
);
if (mongoc_cursor_next(cursor, &doc)) {
char *str = bson_as_canonical_extended_json(doc, NULL);
printf("找到: %s\n", str);
bson_free(str);
}
bson_destroy(query);
mongoc_cursor_destroy(cursor);
这里有个细节:find_one 返回的是游标,哪怕只查一条。你得用 mongoc_cursor_next 去拿结果。如果没找到,mongoc_cursor_next 返回 false。
update_one:更新文档
更新操作需要两个 BSON:一个是查询条件,一个是更新操作。更新操作要用 $set、$inc 这些操作符。我见过新手直接传整个文档进去,结果把其他字段全覆盖了——嗯,这就是没理解 $set 的后果。
bson_t *query = BCON_NEW("name", BCON_UTF8("张三"));
bson_t *update = BCON_NEW(
"$set", "{",
"age", BCON_INT32(30),
"city", BCON_UTF8("深圳"),
"}"
);
bson_error_t error;
if (!mongoc_collection_update_one(collection, query, update, NULL, NULL, &error)) {
fprintf(stderr, "更新失败: %s\n", error.message);
}
bson_destroy(query);
bson_destroy(update);
避坑指南:我曾经在生产环境上犯过一个错——更新条件写得太宽泛,结果一次更新了几千条不该更新的记录。从那以后,我每次写 update 之前,都会先用同样的条件跑一次 find_one 或 count_documents,确认范围没问题再执行更新。
delete_one:删除文档
删除操作跟更新类似,也是先给条件。不过删除更危险,因为数据一旦删了就真没了。我建议你在删除前先备份,或者至少用 find_one 确认一下。
bson_t *query = BCON_NEW("_id", BCON_OID(&oid));
bson_error_t error;
if (!mongoc_collection_delete_one(collection, query, NULL, NULL, &error)) {
fprintf(stderr, "删除失败: %s\n", error.message);
}
bson_destroy(query);
批量操作:bulk_write 的威力
单条操作太慢了,尤其是你要插入一万条数据的时候。MongoDB 提供了批量操作接口,可以把多个操作打包在一起发送,网络开销只有一次。我做过测试,批量插入 10 万条数据,用 insert_one 循环要 30 秒,用批量操作只要 2 秒。
mongoc_bulk_operation_t *bulk;
bson_error_t error;
bulk = mongoc_collection_create_bulk_operation(collection, true, NULL);
// 添加多个插入操作
for (int i = 0; i < 1000; i++) {
bson_t *doc = BCON_NEW(
"index", BCON_INT32(i),
"value", BCON_UTF8("test")
);
mongoc_bulk_operation_insert(bulk, doc);
bson_destroy(doc);
}
// 添加一个更新操作
bson_t *query = BCON_NEW("index", BCON_INT32(500));
bson_t *update = BCON_NEW("$set", "{", "value", BCON_UTF8("updated"), "}");
mongoc_bulk_operation_update_one(bulk, query, update, false);
bson_destroy(query);
bson_destroy(update);
// 添加一个删除操作
bson_t *del_query = BCON_NEW("index", BCON_INT32(999));
mongoc_bulk_operation_remove_one(bulk, del_query, NULL);
bson_destroy(del_query);
// 执行批量操作
if (!mongoc_bulk_operation_execute(bulk, NULL, &error)) {
fprintf(stderr, "批量操作失败: %s\n", error.message);
}
mongoc_bulk_operation_destroy(bulk);
批量操作的关键参数:
ordered:如果为 true,遇到错误就停止;false 则继续执行后续操作。我一般用 false,除非业务要求严格顺序。writeConcern:控制写入确认级别。默认是 majority,性能要求高时可以调低,但数据安全性会下降。
知识体系总览
总结一下
MongoDB 的文档操作,说白了就是 CRUD 的变种。BSON 构建是基础,insert_one、find_one、update_one、delete_one 是四个基本操作,批量操作是性能优化的关键。我个人觉得,掌握这些就够应付 80% 的场景了。剩下的 20%,比如聚合管道、索引优化、事务处理,那是进阶内容,以后有机会再聊。
最后提醒一句:写 C 语言的 MongoDB 程序,内存管理是头等大事。每个 bson_t、mongoc_cursor_t、mongoc_client_t 都要记得释放。我见过太多人程序跑着跑着内存飙到几个 G,最后 OOM 被系统 kill 掉。嗯,别问我怎么知道的。
公众号:蓝海资料掘金营,微信deep3321