3. 数据建模通用方法论

MongoDB 的文档模型设计不是“把表换成集合”这么简单。关系型建模以消除冗余、保证范式为目标,而文档建模的核心目标是:让最常见的查询用最少的读写次数、最小的文档体积完成。因此,从业务需求到最终文档模型,需要经过一条可复用的映射链路:实体识别 → 关系分析 → 访问模式梳理 → 模式设计决策。这条链路不是一次性瀑布,而是“先按访问模式定形状,再用实体和关系校验完整性”的迭代过程。

第一步:实体识别——从业务名词到候选文档

实体识别的输入是业务需求描述、领域术语表和现有系统接口。做法是:把需求中反复出现、有独立生命周期、需要被单独查询的名词圈出来,作为候选实体。例如一个“商品评论与点赞”需求,候选实体包括:用户(User)、商品(Product)、评论(Comment)、点赞记录(Like)。

识别时要区分三类名词:核心实体(有独立标识和生命周期,如 User、Product)、值对象(依附于实体、不需要独立查询,如评论里的“评分 5 星”“图片 URL 列表”)、事件/流水(只追加、按时间查询,如点赞记录、浏览日志)。值对象通常内嵌,事件流水通常独立集合。这一步的产出是一张候选实体清单,每个实体标注:唯一标识、预估量级、读写比例、是否高频更新。

第二步:关系分析——判定 1-1、1-N、N-N 与聚合边界

关系分析决定“谁内嵌谁、谁引用谁”。判定依据是三个问题:子对象是否总随父对象一起读取?子对象数量是否有界?子对象是否被独立查询或独立更新?

  • 1-1:通常内嵌,如用户与其账户设置。
  • 1-N(N 有界且随父读):内嵌数组,如商品与其 5 张主图。
  • 1-N(N 无界或需独立查询):父存子引用或子存父引用,如商品与评论。
  • N-N:用引用数组或中间集合,如用户与关注列表。

这里要引入 MongoDB 的关键约束:单个文档上限 16MB。若子对象无界增长(如一条商品的评论可能上百万),内嵌必然撑爆文档,必须拆成独立集合。因此关系分析的产出是:每个关系的基数、方向、是否内嵌、内嵌后的预估文档大小。

第三步:访问模式梳理——把“怎么查”写成清单

访问模式(Access Pattern)是文档建模真正的设计输入。它描述的是:谁、在什么场景、以什么条件、查什么字段、排序方式、返回多少条、频率多高。建议用一张访问模式表来固化,例如:

编号操作查询条件排序返回量频率
AP-01查看商品详情productId—1 条极高
AP-02查看商品评论首页productIdcreatedAt 倒序20 条高
AP-03查看我的评论userIdcreatedAt 倒序20 条中
AP-04点赞/取消点赞userId+commentId—写入高
AP-05统计评论点赞数commentId—1 个计数高

梳理访问模式时,要特别标注高频、低延迟的查询,它们决定索引和文档形状;低频的统计类查询可以接受聚合管道或离线计算。同时要标注哪些字段需要排序、哪些需要范围过滤,因为 MongoDB 的复合索引遵循 ESR 原则(Equality → Sort → Range)。

第四步:模式设计决策——内嵌、引用与混合

把前三步的结论汇总,对每个关系做决策。以“商品-评论-点赞”为例:

  • 商品与评论:评论无界增长,且 AP-02 按 productId 查、AP-03 按 userId 查,属于双向查询,因此评论独立成 comments 集合,文档内同时存 productId 和 userId,并建 {productId:1, createdAt:-1} 与 {userId:1, createdAt:-1} 两个复合索引。
  • 评论与点赞:AP-05 需要高频读取点赞数,若每次 countDocuments 会拖慢详情页。采用混合模式:点赞明细独立成 likes 集合(保证唯一性与可追溯),同时在评论文档中冗余一个 likeCount 字段,写入点赞时用 $inc 原子更新。
  • 评论中的图片、评分:值对象,直接内嵌为数组和标量字段。

下面这张图展示了从业务需求到文档模型的完整映射链路,以及“商品-评论-点赞”案例中各实体的最终归属与关键索引:

业务需求 → 文档模型的四步映射链路 ① 实体识别 核心实体 / 值对象 / 事件 ② 关系分析 1-1 / 1-N / N-N · 聚合边界 ③ 访问模式梳理 查询条件 / 排序 / 频率 ④ 模式设计决策 内嵌 / 引用 / 混合 按访问模式回校实体与关系(迭代) 案例落地:商品 · 评论 · 点赞 的文档归属 products 集合 _id: ObjectId title / price / category images: [ url, url, ... ] ← 内嵌值对象 commentCount: 1280 ← 冗余计数 索引:{category:1, price:-1} comments 集合 _id / productId / userId content / rating / createdAt likeCount: 36 ← 冗余计数 索引1:{productId:1, createdAt:-1} 索引2:{userId:1, createdAt:-1} likes 集合 _id / userId / commentId createdAt 唯一索引:{userId:1, commentId:1} 防重复点赞 索引:{commentId:1} 1 : N 引用 1 : N 引用 决策依据速查 • 子对象无界增长 → 独立集合(评论、点赞);子对象有界且随父读 → 内嵌(商品图片) • 高频计数读取 → 冗余 likeCount / commentCount,写入时 $inc 原子更新,避免实时聚合 • 双向查询 → 子文档同时保留两个外键,并各建一条 ESR 复合索引;唯一性约束用唯一索引兜底

决策中的常见取舍与校验

完成初版模型后,需要用三个问题做校验。第一,文档大小:估算最大文档体积,若接近 16MB 的 30% 就应警惕,考虑拆分子集合或使用 Bucket 模式。第二,写入热点:若某个文档被高频 $inc 或 $push,要评估是否会造成单文档写竞争;点赞计数这类场景可接受,但若每秒数千次更新同一文档,应考虑分片键或异步合并。第三,索引覆盖:把访问模式表逐条对照索引,确认高频查询都能走索引,且排序字段在索引中的位置符合 ESR 原则。

一个实用习惯:在建模文档里同时维护“访问模式表 + 集合结构 + 索引清单”三份内容,任何一次需求变更都先改访问模式表,再回推集合与索引,避免模型随需求漂移而失控。

最后要强调,文档建模没有唯一正确答案,只有“对当前访问模式更优”的答案。同一组实体,在“读多写少”的详情页场景和“写多读少”的日志场景下,内嵌与引用的选择可能完全相反。因此,方法论的价值不在于给出固定结论,而在于提供一条可重复、可校验、可迭代的推理路径:先识别实体,再分析关系,然后用访问模式定形状,最后用大小、热点和索引三项校验收口。