【MongoDB篇】MongoDB的文档操作!
目录
- 引言
- 第一节:C - Create - 创建文档 (Insert) 👶➕
- 第二节:R - Read - 读取文档 (Query) 📚👀
- 第三节:U - Update - 更新文档 (Update) 🔄✍️
- 第四节:D - Delete - 删除文档 (Delete) 🗑️❌
- 第五节:文档结构与 BSON 数据类型速览 🧱💾
- 第六节:文档操作的考量与最佳实践 📏🔒 atom
- 第七节:总结文档操作!🎉✍️
🌟我的其他文章也讲解的比较有趣😁,如果喜欢博主的讲解方式,可以多多支持一下,感谢🤗!
其他优质专栏: 【🎇SpringBoot】【🎉多线程】【🎨Redis】【✨设计模式专栏(已完结)】…等
如果喜欢作者的讲解方式,可以点赞收藏加关注,你的支持就是我的动力
✨更多文章请看个人主页: 码熔burning
看之前可以先了解一下MongoDB是什么:【MongoDB篇】万字带你初识MongoDB!
引言
好的!咱们一步一个脚印,从“仓库”(MongoDB)走到“房间”(数据库),再打开了“文件柜”(集合)。现在,终于来到了最最核心的地方——柜子里那些装满数据的 “文件”或者说“包裹”——文档 (Document)!📄
了解数据库操作请看:【MongoDB篇】MongoDB的数据库操作!
了解集合操作请看:【MongoDB篇】MongoDB的集合操作!
文档,是 MongoDB 中存储数据的基本单位!你所有实际的数据,不管是用户的名字、年龄、地址,还是商品的价格、库存、描述,亦或是文章的内容、作者、发布日期,统统都是以文档的形式存放在集合里的!
理解文档的操作,就是掌握了与 MongoDB“对话”的绝大部分技能!🚀 准备好了吗?请系好你的数据安全带,咱们要深入文档的奇妙世界了!✨
在 MongoDB 里,一个文档就是一个由键值对 (key-value pairs) 组成的结构。它的长相非常类似于我们熟悉的 JSON 对象!🤩 但在 MongoDB 的内部,文档存储的是 BSON (Binary JSON) 格式的数据。
BSON 相比 JSON 有啥优点?
- 支持更多数据类型:比如日期 (Date)、二进制数据 (Binary data)、ObjectId 等等,这些都是 JSON 原生不支持的。
- 更高效的存储和扫描:作为二进制格式,BSON 在存储空间和读写效率上通常优于纯文本的 JSON。
你可以把文档想象成一个非常灵活的包裹📦,你可以把任何你想放进去的数据(各种类型、各种结构)都塞到这个包裹里。
每个文档都有一个特殊的字段:_id
。🔑
_id
字段是文档在集合中的唯一标识符,就像你的身份证号一样,不能重复!- 如果你在插入文档时没有指定
_id
字段,MongoDB 会自动为你生成一个全球唯一的ObjectId
作为_id
。这个ObjectId
包含了时间戳、机器 ID、进程 ID 和计数器,保证了其在分布式环境下的唯一性。 _id
字段默认会被自动索引,这使得通过_id
查找文档非常快速!⚡
理解了文档的基本概念,接下来,我们就来学习如何对这些文档进行最核心的操作:CRUD (创建、读取、更新、删除)!
第一节:C - Create - 创建文档 (Insert) 👶➕
往集合里“丢”包裹,就是创建文档!MongoDB 提供了两种主要的方法:
-
insertOne(document, options)
:向集合中插入一个文档。use mydatabase; // 确保在正确的数据库 // 插入一个简单的文档 db.users.insertOne({name: "李四",age: 25,city: "上海" });// 插入一个带嵌套文档和数组的文档 db.products.insertOne({name: "MongoDB T-Shirt",price: 25.99,tags: ["t-shirt", "clothing", "mongodb"],details: {color: "blue",size: ["S", "M", "L"]},in_stock: true,created_at: new Date() });// 插入一个指定 _id 的文档 (确保 _id 在集合中唯一) db.users.insertOne({_id: 1001, // 你也可以用数字、字符串等作为 _id,但 ObjectId 是推荐的默认值name: "王五",age: 35 });
document
: 你要插入的文档对象。options
: 可选参数,比如writeConcern
(写入安全级别)。
insertOne()
会返回一个结果对象,其中包含insertedId
,就是插入文档的_id
。 -
insertMany([document1, document2, ...], options)
:向集合中插入多个文档(以文档数组的形式)。db.users.insertMany([{ name: "赵六", age: 22, city: "广州" },{ name: "孙七", age: 29, city: "北京", interests: ["coding", "travel"] },{ name: "周八", age: 31, city: "上海", company: "MongoDB Inc." } ]);
[document1, document2, ...]
: 包含多个文档的数组。options
: 可选参数,比如writeConcern
,或者ordered: boolean
(是否按顺序插入,默认为true
。如果设置为false
,插入过程中即使遇到错误,也会继续插入其他文档)。
insertMany()
会返回一个结果对象,包含insertedIds
,一个包含了所有插入文档_id
的数组。
小贴士: 在进行大量数据导入时,insertMany
通常比循环调用 insertOne
效率更高,因为它减少了网络往返次数。🚀
第二节:R - Read - 读取文档 (Query) 📚👀
从集合里找出你想要的包裹,就是读取文档,也就是我们常说的查询 (Query)!这部分是与 MongoDB 交互中最常用、也最灵活的操作!
核心方法是 find(query, projection)
和 findOne(query, projection)
。
find(query, projection)
:查找符合条件的所有文档,返回一个游标 (Cursor)。游标就像一个指向查询结果集的指针,你可以遍历游标来获取文档。findOne(query, projection)
:查找符合条件的第一个文档。直接返回文档对象(如果找到的话),而不是游标。适合你知道查询结果只有一条或者你只关心第一条的场景。
query
参数 (查询文档):
find()
和 findOne()
方法的第一个参数是一个查询文档 { ... }
,用来指定你想要查找的文档需要满足哪些条件。这是进行查询的关键!
查询选择器 (Query Selectors):
查询文档中,你可以使用各种查询选择器来构建复杂的查询条件。这些选择器通常以 $
开头。哥在之前的“操作大全”里已经列举了一些,这里再针对文档查询,把最常用的详细展开一下,并给出更清晰的例子!
假设我们操作 users
集合,文档结构类似 { name: "...", age: ..., city: "...", interests: [...] }
-
相等匹配:
db.users.find({ name: "李四" }); // 查找 name 等于 "李四" 的文档 db.users.find({ age: 25 }); // 查找 age 等于 25 的文档 db.users.find({ _id: ObjectId("...") }); // 根据 _id 查找文档 (非常高效!)
-
比较查询 (
$gt
,$lt
,$gte
,$lte
,$ne
,$in
,$nin
):db.users.find({ age: { $gt: 30 } }); // 查找 age 大于 30 的文档 db.users.find({ age: { $lte: 25 } }); // 查找 age 小于等于 25 的文档 db.users.find({ city: { $ne: "上海" } }); // 查找 city 不等于 "上海" 的文档 db.users.find({ city: { $in: ["北京", "上海"] } }); // 查找 city 在 "北京" 或 "上海" 中的文档 db.users.find({ interests: { $nin: ["coding"] } }); // 查找 interests 数组中不包含 "coding" 的文档
-
逻辑查询 (
$and
,$or
,$not
,$nor
):// 查找 age > 28 且 city 是“广州”的文档 db.users.find({ $and: [{ age: { $gt: 28 } }, { city: "广州" }] }); // 简写形式 (多个键值对默认就是 AND 关系): db.users.find({ age: { $gt: 28 }, city: "广州" });// 查找 city 是“北京”或 age < 25 的文档 db.users.find({ $or: [{ city: "北京" }, { age: { $lt: 25 } }] });// 查找 age 不大于 30 的文档 (等价于 age <= 30) db.users.find({ age: { $not: { $gt: 30 } } });
-
元素查询 (
$exists
,$type
):db.users.find({ interests: { $exists: true } }); // 查找存在 interests 字段的文档 db.users.find({ company: { $exists: false } }); // 查找不存在 company 字段的文档 db.users.find({ age: { $type: "number" } }); // 查找 age 字段类型是 number 的文档 (数字类型细分为 double, int, long, decimal) db.users.find({ name: { $type: "string" } }); // 查找 name 字段类型是 string 的文档
你可以用
db.mycollection.find().forEach(doc => { for(const key in doc) { print(key + ': ' + typeof doc[key] + ', BSON type: ' + BSON.type(doc[key])); } });
来查看文档中字段的 BSON 类型。 -
评估查询 (
$regex
,$expr
):db.users.find({ name: { $regex: /^李/ } }); // 查找 name 以“李”开头的文档 db.users.find({ name: { $regex: /八$/, $options: 'i' } }); // 查找 name 以“八”结尾的文档,不区分大小写 ('i')// 使用 $expr 查找 age 大于 interests 数组长度的文档 db.users.find({ $expr: { $gt: ["$age", { $size: "$interests" }] } });
-
数组查询 (
$all
,$elemMatch
,$size
):db.users.find({ interests: { $all: ["coding", "travel"] } }); // 查找 interests 数组同时包含 "coding" 和 "travel" 的文档 (顺序不重要)db.users.find({ interests: "coding" }); // 查找 interests 数组包含 "coding" 的文档 (这是 $elemMatch 的简写形式)// 查找 interests 数组中至少有一个元素匹配特定条件的文档 db.users.find({ interests: { $elemMatch: { $regex: /^code/ } } }); // 查找 interests 数组中某个元素以 "code" 开头db.users.find({ interests: { $size: 2 } }); // 查找 interests 数组长度为 2 的文档
projection
参数 (投影文档):
find()
和 findOne()
的第二个参数是一个投影文档 { ... }
,用来指定你希望在返回结果中包含或排除哪些字段。默认情况下,会返回文档的所有字段(包括 _id
)。
{ field: 1, ... }
: 指定需要包含的字段。_id
字段默认包含,如果你不想要_id
,需要显式设置为_id: 0
。{ field: 0, ... }
: 指定需要排除的字段。不能在同一个投影文档中同时使用包含和排除(_id
除外)。
db.users.find({}, { name: 1, city: 1 }); // 查找所有文档,只返回 name, city 字段和 _id
db.users.find({}, { interests: 0 }); // 查找所有文档,排除 interests 字段
db.users.find({}, { name: 1, _id: 0 }); // 查找所有文档,只返回 name 字段,排除 _id
链式调用查询辅助方法:
find()
方法返回的是一个游标,你可以在游标上链式调用各种方法来进一步 refining 你的查询结果:
-
.sort({ field: 1/-1 })
:对结果进行排序。1
升序,-1
降序。db.users.find().sort({ age: -1 }); // 按年龄降序排序
-
.limit(number)
:限制返回的文档数量。db.users.find().limit(5); // 只返回前 5 个文档
-
.skip(number)
:跳过指定数量的文档。常用于分页。db.users.find().skip(10).limit(10); // 跳过前 10 个,返回接下来的 10 个 (第二页)
-
.count()
:计算符合查询条件的文档数量(已废弃,请用countDocuments
或estimatedDocumentCount
)。 -
.countDocuments(query)
:精确计数。 -
.estimatedDocumentCount()
:快速估算总数。db.users.countDocuments({ city: "北京" }); // 精确计算北京用户的数量 db.users.estimatedDocumentCount(); // 快速估算 users 集合总文档数
掌握查询操作,就像拥有了一双“火眼金睛”👀,你可以从海量数据中准确地找到你想要的那一份!多练习各种选择器和组合,你就是查询界的“卷王”!💪
第三节:U - Update - 更新文档 (Update) 🔄✍️
找到文档后,你可能需要修改它里面的内容。这就是更新操作!
核心方法是 updateOne(query, update, options)
, updateMany(query, update, options)
, 和 replaceOne(query, replacement, options)
。
updateOne(query, update, options)
:更新符合query
条件的第一个文档。updateMany(query, update, options)
:更新符合query
条件的所有文档。replaceOne(query, replacement, options)
:用replacement
文档完全替换符合query
条件的第一个文档。
update
参数 (更新操作文档):
更新操作(除了 replaceOne
)的核心在于使用更新操作符 (Update Operators)!你不能直接传一个 { field: new_value }
这样的文档去更新,那样会替换整个文档(除非你用的是 replaceOne
)!你需要用 $set
, $inc
等操作符告诉 MongoDB 如何修改文档。
更新操作文档的结构通常是 { $operator: { field: value, ... }, $anotherOperator: { ... } }
。
更新操作符 (Update Operators):
哥在之前的“操作大全”里也介绍了一些,这里结合文档操作,再详细讲讲最常用的!
假设我们操作 users
集合
-
字段更新操作符 (
$set
,$unset
,$inc
,$mul
,$rename
):-
$set
: 设置或修改字段的值。如果字段不存在,则添加该字段。最常用!// 将 李四 的 age 改为 26 db.users.updateOne({ name: "李四" }, { $set: { age: 26 } }); // 给 李四 添加一个 email 字段 db.users.updateOne({ name: "李四" }, { $set: { email: "lisi@example.com" } }); // 同时修改多个字段 db.users.updateOne({ name: "李四" }, { $set: { age: 27, city: "深圳" } });
-
$unset
: 删除字段。值不重要,只要存在就行。// 删除 李四 的 email 字段 db.users.updateOne({ name: "李四" }, { $unset: { email: "" } });
-
$inc
: 字段值增加指定的数值。// 给所有用户的 age 增加 1 db.users.updateMany({}, { $inc: { age: 1 } });
-
$mul
: 字段值乘以指定的数值。// 将 李四 的 age 乘以 2 db.users.updateOne({ name: "李四" }, { $mul: { age: 2 } });
-
$rename
: 重命名字段。// 将所有文档的 interests 字段重命名为 hobbies db.users.updateMany({}, { $rename: { interests: "hobbies" } });
-
-
数组更新操作符 (
$push
,$addToSet
,$pop
,$pull
,$pullAll
,$each
,$slice
,$sort
,$position
):-
$push
: 向数组末尾添加一个元素。// 给 赵六 的 hobbies 数组添加一个兴趣 "看电影" db.users.updateOne({ name: "赵六" }, { $push: { hobbies: "看电影" } });
-
$addToSet
: 向数组中添加一个元素,只有当该元素不在数组中时才添加(保证唯一性)。// 给 赵六 的 hobbies 数组添加 "听音乐",如果已经有了就不添加 db.users.updateOne({ name: "赵六" }, { $addToSet: { hobbies: "听音乐" } });
-
$pop
: 从数组的头部 (-1
) 或尾部 (1
) 删除一个元素。// 删除 赵六 的 hobbies 数组的最后一个元素 db.users.updateOne({ name: "赵六" }, { $pop: { hobbies: 1 } }); // 删除 赵六 的 hobbies 数组的第一个元素 db.users.updateOne({ name: "赵六" }, { $pop: { hobbies: -1 } });
-
$pull
: 从数组中删除所有匹配指定条件的元素。// 删除 赵六 hobbies 数组中所有值为 "看电影" 的元素 db.users.updateOne({ name: "赵六" }, { $pull: { hobbies: "看电影" } });
-
$pullAll
: 从数组中删除所有匹配指定值列表的元素。// 删除 赵六 hobbies 数组中值为 "听音乐" 和 "爬山" 的元素 db.users.updateOne({ name: "赵六" }, { $pullAll: { hobbies: ["听音乐", "爬山"] } });
-
$each
,$slice
,$sort
,$position
: 与$push
结合使用,用于批量添加、限制数组大小、排序、指定插入位置。// 给 赵六 的 hobbies 数组添加多个兴趣 ["跑步", "游泳"] db.users.updateOne({ name: "赵六" }, { $push: { hobbies: { $each: ["跑步", "游泳"] } } });// 添加一个兴趣,并保留 hobbies 数组的最后 3 个元素 db.users.updateOne({ name: "赵六" }, { $push: { hobbies: { $each: ["徒步"], $slice: -3 } } });// 添加一个兴趣,并对 hobbies 数组进行升序排序 db.users.updateOne({ name: "赵六" }, { $push: { hobbies: { $each: ["滑雪"], $sort: 1 } } });// 在 hobbies 数组的第二个位置 (索引 1) 插入一个兴趣 "摄影" db.users.updateOne({ name: "赵六" }, { $push: { hobbies: { $each: ["摄影"], $position: 1 } } });
-
replaceOne(query, replacement, options)
:
这个操作不使用更新操作符,而是用 replacement
文档完全替换符合 query
条件的第一个文档。原文档中没有在 replacement
中出现的字段都会被移除!
// 找到名字是"李四"的文档,用一个新的文档完全替换它
db.users.replaceOne({ name: "李四" }, { name: "李四 New", status: "active", last_updated: new Date() });
// 原文档中李四的 age, city 字段会被移除,只剩下 name, status, last_updated 和 _id
options
参数 (更新选项):
updateOne
, updateMany
, replaceOne
都支持一些选项,最常用的是 upsert
。
-
upsert: boolean
:如果设置为true
,当没有找到符合query
条件的文档时,MongoDB 会根据query
和update
或replacement
参数插入一个新的文档。// 尝试更新名字是"不存在的人"的文档,如果不存在,就插入一个新文档 db.users.updateOne({ name: "不存在的人" },{ $set: { status: "inactive" } },{ upsert: true } // 因为不存在,所以会插入 { name: "不存在的人", status: "inactive", _id: ... } );
更新操作是 MongoDB 灵活性的重要体现。通过各种更新操作符,你可以对文档的结构和内容进行非常精细和高效的修改!熟练掌握它们,你的文档操作能力将大幅提升!🚀
第四节:D - Delete - 删除文档 (Delete) 🗑️❌
不需要的文档,直接“扔掉”!🗑️
核心方法是 deleteOne(query, options)
和 deleteMany(query, options)
。
deleteOne(query, options)
:删除符合query
条件的第一个文档。deleteMany(query, options)
:删除符合query
条件的所有文档。
query
参数 (查询文档):
跟查询操作一样,delete
操作的第一个参数也是一个查询文档 { ... }
,用来指定要删除哪些文档。
use mydatabase; // 确保在正确的数据库// 删除名字是"不存在的人"的文档
db.users.deleteOne({ name: "不存在的人" });// 删除所有年龄小于 25 的文档
db.users.deleteMany({ age: { $lt: 25 } });// 删除所有 city 是"上海"且 age 大于 30 的文档
db.users.deleteMany({ city: "上海", age: { $gt: 30 } });// **危险操作!** 删除 users 集合中的所有文档!务必谨慎!💣💥
db.users.deleteMany({});
警告: 删除操作是不可逆的!被删除的文档无法直接恢复!在执行 deleteMany({})
这样的操作之前,一定要再三确认你的意图和目标集合!否则,你可能会失去所有宝贵的数据!😭
第五节:文档结构与 BSON 数据类型速览 🧱💾
一个文档就是由键值对组成的 BSON 对象。键(key)是字符串,值(value)可以是各种 BSON 支持的数据类型。
常见的 BSON 数据类型包括:
- String:字符串
- Integer (32-bit / 64-bit):整数,MongoDB 会根据数值大小自动选择存储类型
- Double:双精度浮点数
- Boolean:布尔值 (
true
或false
) - Array:数组,值可以是任何 BSON 数据类型,可以嵌套数组。
- Object / Embedded Document:嵌套文档,文档中可以包含子文档,形成层级结构。
- ObjectId:12 字节的唯一 ID,MongoDB 自动生成,通常用作
_id
。 - Date:日期和时间,存储为 Unix 时间戳(自 epoch 以来的毫秒数)。
- Binary Data:二进制数据。
- Null:表示字段值为 null。
- Undefined:表示字段未定义 (已废弃,不推荐使用)。
- Regular Expression:正则表达式。
- Timestamp:时间戳,主要用于内部操作,比如复制集。
- Decimal128:高精度十进制数,适合处理货币等需要精确计算的场景。
理解这些数据类型,有助于你更好地设计文档结构和进行数据操作。
第六节:文档操作的考量与最佳实践 📏🔒 atom
进行文档操作时,有一些重要的考量和最佳实践:
- 文档大小限制:MongoDB 对单个文档的大小有限制,目前是 16MB。如果你的文档超过这个大小,将无法插入或更新。设计文档结构时要考虑到这一点。对于大型二进制数据(如图片),通常建议存储在文件存储系统(如 S3、GridFS 等)中,文档中只存储文件引用。
_id
的重要性:_id
是文档的唯一标识,并且自动带索引,通过_id
进行查询非常高效。在设计文档时,思考如何利用_id
或其他唯一字段来快速定位文档。- 内嵌文档 vs 引用:在设计一对一或一对多关系时,你可以选择使用内嵌文档(将相关文档直接嵌套在父文档中)或者使用引用(在父文档中存储相关文档的
_id
)。- 内嵌:查询效率高(无需 JOIN),但可能导致文档过大、数据冗余(如果内嵌的数据经常被修改)。
- 引用:避免文档过大和数据冗余,但查询时可能需要进行额外的查询(类似 JOIN)。
选择哪种方式取决于你的访问模式:如果内嵌数据经常与父文档一起访问,且不经常更新,内嵌是好选择;如果内嵌数据很大或经常独立更新,引用可能更好。
- 单文档原子性:在 MongoDB 中,对单个文档的更新操作是原子性的。这意味着你在修改一个文档时,要么所有修改都成功,要么都不成功,不会出现修改了一半的情况。但是,跨文档的操作默认不是原子性的!如果你需要保证多个文档操作的原子性,就需要使用多文档事务(如我们之前提到的)。
- 权限:进行文档的增删改查操作,需要用户拥有对应集合所在数据库的
read
,readWrite
,update
,remove
等角色权限。
第七节:总结文档操作!🎉✍️
恭喜你!🎉 你已经掌握了 MongoDB 最核心的知识点——文档的 CRUD 操作!从创建、读取(包括各种复杂的查询)、更新(包括强大的更新操作符)到删除,你已经具备了与 MongoDB 数据库中的数据进行交互的基本能力!
文档是 MongoDB 的灵魂,灵活的文档模型和强大的操作符让你可以非常自由地处理各种数据结构。
希望这篇关于文档的详细讲解对你有所帮助!实践是最好的老师,现在就去你的 MongoDB 环境里,尽情地对文档进行 CRUD 操作吧!去玩转你的数据!😎