☰
MongoDB嵌入式文档实战:数据建模中的嵌入与引用选择
2026/10/14 22:52:22 网站建设 项目流程

1. 从一次数据建模选择说起——为什么嵌入式文档值得单独讲

大概两年前,我在某移动端项目里做订单模块的数据建模。第一次用 MongoDB 时,我下意识地把订单拆成了orders、order_items、products三个集合,然后靠关联查询把它们拼起来——这套思路是从 SQL 时代带过来的,手很熟,但跑起来总觉得别扭。后来一位做过电商中台的同事看了一眼我的表结构,反手甩给我一份我们后来的订单文档:orders集合里,每个订单文档直接内嵌了一个items数组,数组里放的是商品快照。我当时的第一反应是“这不符合规范化”,第二反应是“等等,这读起来也太方便了”。

这个场景,几乎是每位 MongoDB 新手都会撞上的坎:面对一条业务数据,到底该不该把它的子数据直接塞进同一个文档里?这个“塞进去”的行为,就是 MongoDB 里最核心也最容易被误解的概念之一——嵌入式文档(Embedded Document)。

如果你正在学 MongoDB,或者已经在项目里用它,但每次建模时都在“嵌入”和“引用”之间反复横跳,那这篇内容就是为你准备的。我会从嵌入式文档的定义讲起,接着聊它为什么好用、边界在哪里、什么时候必须放弃它,最后再分享几个我在实际项目里踩过的坑和现在养成的一些建模习惯。这应该是整个 MongoDB 系列里最贴近“怎么设计数据”的一篇,强烈建议你静下心看完。

2. 什么是嵌入式文档:文档套文档,在 MongoDB 里的具体含义

2.1 一句话定义和两个典型例子

嵌入式文档,说白了就是:一个文档的某个字段,它的值本身又是一个文档,或者一个由多个文档组成的数组。换句话说,MongoDB 的文档结构是支持“嵌套”的,JSON 能做到的那种套娃,在 MongoDB 里完全合法,而且正是它的核心玩法。

光说概念太干,直接上代码。我负责的那个电商项目,用户信息就这么存:

db.users.insertOne({ _id: ObjectId("..."), username: "xiaoming", profile: { fullName: "小明", gender: "male", birthDate: ISODate("1998-06-12") }, address: { province: "广东省", city: "深圳市", district: "南山区", detail: "某科技园某栋某室" } })

这个文档里,profile字段的值是一个文档,address字段的值也是一个文档。它们不是“关联到其他集合的某个 _id”,而是真真切切地被存放在了users集合的这个文档内部。

再来看项目里订单模块的文档设计。订单文档内嵌了一个商品快照数组,数组里每个元素又是一个独立的文档:

db.orders.insertOne({ _id: ObjectId("..."), orderNo: "DD20240618001", userId: ObjectId("用户的_id"), status: "paid", items: [ { productId: ObjectId("商品的_id"), productName: "无线蓝牙耳机", spec: "白色降噪版", unitPrice: 299, quantity: 2, subtotal: 598 }, { productId: ObjectId("商品的_id"), productName: "快充充电器", spec: "Type-C 65W", unitPrice: 89, quantity: 1, subtotal: 89 } ], totalAmount: 687 })

注意一个细节:items数组里的商品,每一份都完整保存了当时的商品名称、规格、单价。它叫“快照”,因为用户在买东西的那一刻,这些信息被原样拷贝了一份进订单。以后商品再涨价、改名,都不影响这笔历史订单的数据。这种“把旧数据封存在文档内部”的做法,是嵌入式文档最有魅力的应用场景之一。

2.2 底层存储逻辑:BSON 与数据局部性

嵌入式文档能被“塞进”同一个文档里,万法归一,还是要看 MongoDB 的存储层。MongoDB 的文档在内存里是 BSON 格式,BSON 的底层二进制结构天然支持嵌套,当你把一个嵌入式文档写入某个集合时,这段嵌套数据不是分散存在磁盘的不同区域,而是和父文档作为一个连续的整体存储。

这带来一个很重要、也很容易被人忽略的特性:数据局部性(Data Locality)。

打个比方,你可以把整个 MongoDB 文档想象成一个实体档案袋。你往这个档案袋里塞了“基础信息”三张纸、“地址”两张纸、“订单商品”五张纸,这些纸全部叠在一起,装在同一个档案袋里。查询时,只要把这个档案袋从档案架上取下来,里面的所有纸张一次性全部拿到手。而关系型数据库的那种关联查询,相当于你手上有 A、B、C 三个档案袋,分别放在档案架的不同位置,为了凑齐一次完整的信息,你得伸手去拿三次,甚至更多。

单文档读取时,这种物理连续存储的优势会被放大。当你findOne一条订单时,MongoDB 只需要做一次磁盘 IO(多数情况下直接在内存里命中),就能把订单头部、商品明细、收货地址、支付信息全部读出来。如果用引用式设计,同一份信息可能分散在订单集合、商品集合、地址集合,你要么发多次查询,要么用$lookup做聚合联表,代价都会大不少。

2.3 嵌入式文档的“物理边界”:16MB 与 100 层

聊到这儿,有一个硬指标必须提:单个 MongoDB 文档的大小上限是 16MB。嵌入式文档作为文档的一部分,同样受这个约束。也就是说,你不能把一个文档设计成“无限往里塞数据”的形状,因为 16MB 的物理上限就摆在那儿。

还有一个相对冷门的限制:BSON 文档的嵌套层级目前不能超过 100 层。不过正常业务设计极少用到那么深,我自己做过的最极端的设计也就是三层嵌套,再深基本是设计出了问题,而不是业务真的有这个需要。知道有这层限制,正常建模基本碰不到它。

3. 嵌入式的价值在哪里:从“读”这个动作出发来看

3.1 一次读取,得到完整业务对象

嵌入式文档最直接的好处,是我开头提到的那句话:一次查询,把完整业务对象拿全。

拿博客系统举例。一篇博客文章需要展示标题、正文、作者名、发布时间,而文章下方通常要带评论列表。我见过两种常见设计:

第一种,参考式设计:posts集合存文章,comments集合存评论,评论里带一个postId。前端要展示一篇带评论的文章时,得先查文章,再查评论,或者用$lookup把两个集合数据拼起来。注意,$lookup虽然能做到“一条聚合指令”,但它背后的代价是额外的集合访问和数据拼接,性能天然不如“一个文档直接自带数据”。

第二种,嵌入式设计:posts集合的每个文档里,直接内嵌一个comments数组。

db.posts.insertOne({ _id: ObjectId("..."), title: "深入理解嵌入式文档", author: "小明", content: "……", publishedAt: ISODate("..."), comments: [ { userId: ObjectId("..."), nickname: "小李", content: "讲得很清楚", createdAt: ISODate("...") }, { userId: ObjectId("..."), nickname: "小王", content: "收藏了", createdAt: ISODate("...") } ] })

后端只需要一条查询:

db.posts.findOne({ _id: ObjectId("...") })

连带评论一起回来。接口层不用做任何组装,前端拿到的 JSON 直接可用。这种“查询次数最少化”的思路,在读写比例高的业务里能省掉大量的网络往返和计算开销。

3.2 单文档原子性:一个重要但容易被忽略的好处

MongoDB 的原子性作用域是“单个文档”。也就是说,一个文档内部的读写操作,无论牵扯多少字段、多少数组元素,都是原子的。

这个特性在嵌入式文档上体现得非常充分。还是拿订单场景说:一个订单文档里有status字段,有items数组,有totalAmount。你需要实现“订单已支付,把订单状态改成已支付,同时每个商品标记为已结算”这样的逻辑。如果用引用式设计,这些数据分散在不同集合,你需要发起多次写入来保证一致性——要么引入事务,要么接受一段时间的数据不一致。但如果用嵌入式设计,一切都落在同一个文档里,一次updateOne就能保证原子完成,不用额外开事务。

我在项目中经常跟小伙伴强调一句话:能用单文档原子性解决问题,就别急着上事务。事务不是不能用,而是它有额外的协调开销和跨分片的限制。在很多业务场景下,把逻辑上要求“同生共死”的数据嵌进同一个文档,是让一致性需求变得简单的最优雅路径。

3.3 访问模式优先:你不是在存数据,你是在服务读请求

这一点是做 MongoDB 建模和做关系型建模最根本的思维差异。

关系型数据库建模的起点是“实体和实体之间的关系”,所以你会先画 ER 图,把用户、订单、商品拆成不同的表,再用主外键把它们连起来。这个做法在规范化方面很漂亮,但代价是读取经常要 join。

MongoDB 的建模起点应该是:当用户发起一个请求时,你最希望用几次查询、多快的速度、把什么形状的数据交出去?换句话说,先想读,再想存。嵌入式文档就是“为了让读更舒服”而生的设计。它允许你把一个完整业务场景所需的数据,打包在一个物理文档里。这种反范式的“冗余”,恰恰是它在特定场景下性能拔尖的原因。

4. 嵌入的边界:什么时候它反而会成为负担

讲完收益,必须泼一盆冷水。嵌入式文档不是万能药,如果选错场景,它会变成项目的定时炸弹。以下是我在实际开发中真正遇到过的问题。

4.1 无限增长的数组:从便捷变成灾难

最典型的问题出现在“集合内数组无限增长”的设计上。

我做过的某内容社区项目,早期设计时,把文章评论直接嵌在文章文档里。文章热度低时一切安好;可一旦出现一篇爆款,评论数量从几十条冲到几千条,问题就来了。首先,文档体积持续膨胀,离 16MB 的物理上限越来越近;其次,每次新增一条评论,MongoDB 都要对整篇文档做一次更新操作,写放大非常严重;更要命的是,热门文章的并发评论很容易造成文档层面的写冲突,两个用户同时提交评论,就可能有一个请求失败重试。

回过头再看,这种场景的正确做法是把评论抽成独立集合,评论里引用文章_id。独立集合的文章评论查询,配合索引和分页,再热门的文章也不至于把单个文档压垮。

我后来总结了一个经验公式:如果某个嵌入的数组会随着用户行为无上限增长——比如评论、点赞记录、操作日志——请条件反射般地把它拆出去。数组适合承载“有限的数量”,不适合承载“无限的水流”。

场景是否适合嵌入原因
订单里的商品快照适合数量少且固定,伴随订单整体读写
文章底下的评论不适合(除非做数量封顶)可能无限增长,写入频繁,易导致文档膨胀
用户资料里的地址列表适合(小规模)数量少,和用户一起读取频率高
好友关系不适合多对多关系,任何一方的增删都要改动对方文档,耦合过强

4.2 数据冗余的一致性治理成本

嵌入式文档鼓励冗余。你不能幻想“地址修改一次,所有关联数据自动更新”,因为根本没有外键帮你做这件事。

举个我实际踩过的例子。某管理后台系统里,有一张“项目团队表”,团队负责人信息被直接嵌在了团队文档里。某天这位负责人离职,新负责人上任,需要更新所有涉及该负责人的团队记录。多少个团队引用了他的名字?答案是 40 多个。当年建表时图方便嵌入,修改时就要一条一条地把每个团队文档翻出来更新,整个过程既不优雅也容易遗漏。

这个问题的本质是:嵌入的数据到底是一份“不可变的快照”,还是一份“需要保持多副本一致的可变主数据”?如果它属于后者,你就要为每一次修改付出额外的一致性维护成本。设计之初就要想清楚,愿不愿意承担这份成本。

4.3 引用式方案的可用性:$lookup 并非不能碰

有人可能会问:既然嵌入式有这么多限制,那我是不是永远应该用引用式?也不是。引用式最常用的查询方式$lookup聚合阶段有它自己的问题:它本质上是一次“类似 join”的操作,涉及到跨集合的数据读取和内存拼接,在数据量大、聚合管道复杂时,性能会明显不如嵌入式方案的“一次文档读全”。

但$lookup也有它的历史使命:它让 MongoDB 在面临“必须跨集合关联”的需求时不至于黔驴技穷。业务早期数据量小,用引用式加$lookup完全没问题;数据量大了以后,可以通过冗余、缓存、分片等手段做优化。所以别把$lookup当成洪水猛兽,也不要为了追求“性能极致”而把所有数据都塞进一个文档,最终做出一个重达 15MB 的巨型文档。

5. 决策模型:当我面对“嵌入还是引用”时,我会问自己哪四个问题

很多朋友希望我给出一个“一劳永逸的规则”,比如“一对多用嵌入,多对多用引用”。可惜现实中的建模决策没有这么简单,数据之间到底是“一对多”“一对一”还是“多对多”,只靠关系类型判断是远远不够的。

我现在做决策时,会按照顺序追问自己四个问题。它们比单一的关系判断更可靠。

5.1 这四个问题具体是什么

第一问:我是不是总需要一次性读取这些数据?如果文章和评论总是同时被展示,那么嵌在一起能省一次查询;如果某些数据只是偶尔被一起访问,那么嵌入带来的“省查询”价值就不明显,反而要承受更新时的耦合成本。

第二问:这个嵌入的部分会被单独修改吗?如果会被频繁、独立地修改,比如用户头像字段嵌在文档里,但用户动不动就换头像,每一次换头像都要改主文档,写放大会很明显。反过来,订单里的商品快照一旦生成就不再变,嵌入没有任何副作用。

第三问:这个嵌入的部分会无限增长吗?会,就别嵌;不会,或者你有明确的上限,比如“一个商品规格最多五个”,那嵌入是安全的。

第四问:它需要被别的文档引用吗?如果商品信息会被订单引用、会被库存引用、会被店铺页面引用,那么商品应当作为独立集合存在,其他业务引用它的_id或快照字段,而不要试图把它塞进所有业务文档里。

5.2 用一个具体例子完整走一遍决策流程

假设我们在设计一个电商系统的“商品详情”功能。商品有自己的基础属性:名称、价格、库存、描述;还有规格参数:尺寸、材质、颜色等。这些数据之间是什么关系?一个商品对应一组规格。

用这套流程来推演:

  • 商品和规格是否总是需要一次性读取?是,因为商品详情页一定同时展示商品基础信息和规格。
  • 规格会被单独修改吗?会,运营人员经常单独维护规格参数。但注意,这种修改频率远低于查询频率,且规格是商品的一部分,修改商品文档并不算灾难。
  • 规格会无限增长吗?不会,每个商品的规格数量固定,顶多几十个。
  • 规格会被别的文档引用吗?通常不会,它属于商品自身的属性。

结论:商品文档内嵌规格参数,是一个非常合理的嵌入式文档设计。实际操作中,product.specs存放一个文档数组,specs数组里每个元素是{ name: "材质", value: "不锈钢" }这样的结构。访问端口直接读一条商品文档,什么都不用拼。

再拿“商品 vs 订单”的关系走一遍流程。一个订单包含多个商品,一个商品可以出现在多个订单里,看起来是典型多对多。但订单里真正需要的不是“实时商品数据”,而是“下单那一刻的商品快照”,订单和商品之间并不需要保持一致。这时嵌入式快照设计反而是最佳选择:订单文档嵌items数组,保存商品名称、单价、规格等信息,为的是订单随时可读,且不受商品后续变价、下架的干扰。商品的基础数据依然独立存在products集合里,二者互不干扰。

5.3 一个需要特别谨慎的“中间地带”

还有一种常见情况:两个集合的数据经常要一起读,但其中一方数据很大或者更新很频繁。比如“用户”和“用户最近登录记录”。把它们嵌在一起,每次登录都更新用户文档,可能造成文档热点冲突;拆成两个集合,读用户信息时又要多一次查询。

这种场景我会采用折中方案:主文档嵌入“最近一次登录信息”(比如时间、IP、设备),历史登录记录放独立集合。这样既能满足“大多数时候只需要看最近一条记录”的读取需求,又避免了登录日志无限增长拖垮主文档。这种“有损冗余”的思路,是嵌入式设计决策里比单纯“能嵌就嵌”高一个段位的玩法。

6. 实战中的三个高频坑:我在嵌入式文档上踩过的雷

6.1 坑一:数组内嵌文档的更新,居然写错了路径

先说一个看起来不难、实操里特别容易翻车的场景:更新嵌入式数组里某个具体元素。

比如我有这样一个订单文档:

{ _id: ObjectId("..."), items: [ { name: "耳机", price: 299 }, { name: "充电器", price: 89 } ] }

现在要把第一个商品的价格改成 199。很多人直接这样写:

db.orders.updateOne( { _id: ObjectId("...") }, { $set: { items.price: 199 } } )

这必错。items是一个数组,不是“某个字段”,不能直接用点号加price去定位。正确写法至少有三种。最简单的是按下标定位:

db.orders.updateOne( { _id: ObjectId("...") }, { $set: { "items.0.price": 199 } } )

下标写死的缺点很明显:你得先知道目标元素在哪个位置。更通用的做法是用$[elem]数组过滤器,按条件更新所有匹配的元素:

db.orders.updateOne( { _id: ObjectId("..."), "items.name": "耳机" }, { $set: { "items.$[elem].price": 199 } }, { arrayFilters: [{ "elem.name": "耳机" }] } )

这一段代码,我是真的在项目里改错过很多次才彻底记住的。新手最容易忽略的细节是:更新条件里也要带上items.name这个字段(用于匹配文档),arrayFilters里再带elem.name(用于匹配数组元素),两步缺一不可。如果你只写了arrayFilters而忘了在查询条件里过滤数组元素,MongoDB 会先把所有包含“耳机”元素的文档全找出来,再逐个定位修改,虽然结果大概率也对,但多扫描了数据,性能上不划算。

6.2 坑二:嵌入数组的追加和删除,别只知道 $push

“往数组里加一个元素用$push”,这个很多人都知道。但如果你只知道$push,迟早会遇到两个问题:数组无限膨胀,以及如何按条件精准删除。

场景一:记录某用户在社区里的“最近浏览记录”,最多保留 10 条。可以用$push配合$slice来做:

db.users.updateOne( { _id: ObjectId("...") }, { $push: { recentViews: { $each: [{ productId: ObjectId("..."), viewedAt: new Date() }], $position: 0, $slice: 10 } } } )

$slice会保证数组永远不超过 10 条,新记录插在最前,最老的自动被裁剪。这个用法是“嵌入式数组”场景下的高频救星。

场景二:删除数组里某个特定的元素。别用$set将它置为空,那只会留下一个空文档。正确姿势是$pull配合条件:

db.users.updateOne( { _id: ObjectId("...") }, { $pull: { recentViews: { productId: ObjectId("...") } } } )

只要匹配productId这个条件,就能把整个元素从数组里摘干净。类似地,修改数组里的某个子元素用$set加位置或过滤器,追加用$push,删除用$pull,这四个操作基本覆盖了嵌入式数组的日常维护。别把它们记混了,否则你会和我一样,在一行 update 上耗掉半小时的定位时间。

6.3 坑三:查询嵌入式字段,用了不合适的写法

嵌入式文档的查询,看起来是人畜无害的find({ "profile.city": "深圳" }),但只要稍微深入一点,就藏着一个大坑。

假设我在项目里要查出所有住在深圳的用户。一个常见的错误是全字段匹配:

db.users.find({ address: { province: "广东省", city: "深圳市", district: "南山区", detail: "某科技园某栋某室" } })

这条查询要求address的完整结构、字段顺序都完全一致,才能匹配成功。只要detail不一样就查不到。所以正确的做法一定是点号路径定位字段:

db.users.find({ "address.city": "深圳市" })

这条查询只关注address对象的city字段,不管其他字段如何,都能正确命中。同理,查询数组内嵌文档时也是用点号路径:

db.orders.find({ "items.productName": "无线蓝牙耳机" })

另外,如果为了提升查询性能,还需要给这个内嵌字段建索引,比如:

db.users.createIndex({ "address.city": 1 })

MongoDB 的索引天生支持这种“点号路径”的创建方式,无需先把嵌套字段拆出来。这一点和 MySQL 里“对 JSON 子字段建索引”完全不是一个上手难度。

7. 嵌入式文档和引用式方案:什么时候选哪个,我的最终判断习惯

把上面所有内容浓缩成一张参考表,方便你在项目里随时翻阅。

评估维度使用嵌入式文档使用引用式
读取模式父文档和子数据总是同时读取各部分数据经常被单独读取
写频率子数据很少改动,或改动频率远低于查询子数据改动频繁,且多处引用需要同步
数据量增长数量可控,有明确上限数量可能无上限扩展
关系类型一对一、一对多且量小多对多、树形结构、复杂关系网
原子性需求需要单文档原子性保证允许跨文档分阶段写入或引入事务
典型场景订单含商品快照、用户含地址、商品含规格文章含评论、用户与用户的好友关系、日志与主实体

这张表只能帮你起步。真正决定“要不要嵌入”的标尺,是你对业务访问模式的判断,而不是某个刻板的规则。我至今没有找到一条放之四海而皆准的数学公式,如果有的话,MongoDB 官方也就不用写那么长一篇数据建模指南了。

还有个非常有用的实操建议:设计阶段先在纸上画出这个文档的“最终形态”,然后问自己——如果文档一天后被读取十亿次,这个形状读起来爽吗?如果文档的某个字段每秒钟被修改十次,这个形状写起来疼吗?这两个问题的答案,往往比任何规则手册都来得直接。

8. 嵌入式文档的进阶玩法:聚合管道让这套设计如虎添翼

读完前面这些,你大概已经理解了嵌入式文档的设计原则。但真正让嵌入式文档“武器化”的,是它配合聚合管道使用时那手“既整合又不整合”的拿捏。

我在项目里大量使用的场景,是$unwind。它可以把嵌入式数组里的每个元素“拍平”成独立的行,然后跟其他阶段的聚合操作衔接起来。

举个例子,运营要统计每个商品被下单的总数量。订单文档里内嵌了items数组,数组里每个商品有productName和quantity。如果数据没有嵌入,你得先查订单再查商品,麻烦;但现在因为嵌在同一个文档里,只需要这样:

db.orders.aggregate([ { $unwind: "$items" }, { $group: { _id: "$items.productId", totalQuantity: { $sum: "$items.quantity" } } }, { $sort: { totalQuantity: -1 } } ])

$unwind先展开数组,$group再按商品分组求和。没有一次 join,没有跨集合访问,全部操作都在单个集合的文档内部完成,性能和逻辑清晰度都很理想。

类似的还有$project投影输出,可以只返回文档中的某个内嵌子文档,而不必把整个大文档拖出去传输。比如用户接口只需要返回用户的profile,没必要把订单记录也一起返回:

db.users.find( { _id: ObjectId("...") }, { "profile.fullName": 1, "profile.gender": 1 } )

在嵌入式文档的模型下,这种“按需投影”会让接口层的数据裁剪变得非常轻巧。你在设计 API 时,甚至可以直接把文档形状理解为接口响应结构,一个文档映射一个接口,后端代码的组装逻辑大幅减少。

我个人还喜欢把聚合管道比作“一把瑞士军刀”:它既能展开嵌入式数组($unwind),也能拼接引用数据($lookup),还能重塑文档结构($project、$addFields)。但工具再厉害,也要建立在合理的数据模型之上。如果你把数据设计成了一团乱麻的深层嵌套,聚合管道写起来也会痛不欲生。合理的嵌入式设计,是让聚合管道发挥真正实力的前提。

9. 看到这里,你该带走的建模心法

如果你现在去翻 MongoDB 官方文档,关于“嵌入式文档”的定义可能只有几句话。但真正在项目里做决策时,你会发现影响因子多到能写一本书。我并不想给你一套高深莫测的模型理论,只想分享几个我这两年反复验证后的朴素心法。

第一,模型永远是读模型的奴隶。别再把关系型数据库的 ER 图思维原封不动带进 MongoDB。每当你准备拆分集合,先问问自己:这个数据要被怎么读?读的频率多高?当读取结果永远是那一个固定形态时,嵌入式文档往往是性价比最高的选择。

第二,冗余不是脏数据,是设计。订单里存商品快照是冗余,但它保证了订单的历史不可篡改性。用户资料里存一份地址副本是冗余,但它免去了每次查询都要关联地址表的开销。区分“好冗余”和“坏冗余”的标准很简单:这个冗余字段是不是写多读少、改了还得到处同步?如果是,那它就是坏的味道;如果它是不可变或者极少变的快照,那它是珍贵的设计。

第三,给“无限”留一条安全通道。凡是会无限增长的数据——评论、日志、聊天记录、操作流水——在一开始就要考虑拆成独立集合。这不是让你放弃嵌入式文档,而是让你知道文件的边界在哪里。写代码时,16MB 的上限不会每天都碰到,但文档从 3MB 长到 15MB 的过程,会真实地发生在你几乎无感的某一天,到那时,数据迁移的痛苦可远大于建模时提前拆分的几分钟思考。

最后聊点实在的:如果你刚接触 MongoDB,建议从今天开始把你熟悉的某张关系型表结构重写成嵌入式文档方案,在本地跑一遍增删改查和聚合,体会一下那些曾经需要 join 的查询是如何变简单的。这个过程会帮你把“嵌入式文档”从概念层面彻底变成手感。我当初就是这么练的——拿一个小项目反复改数据模型,改到看见一个业务场景就能条件反射地画出最优文档形状。这个手感一旦有了,你在 MongoDB 的世界里就算真正入门了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询