MongoDB文档关系详解

MongoDB作为非关系型文档数据库,摒弃了MySQL的外键关联机制,通过内嵌文档引用关联两种方式实现实体间关系建模。本文将从零讲解MongoDB核心的一对一、一对多、多对多文档关系,结合真实业务场景,提供完整的新增、查询实操代码,同时总结三种关系的建模选型规则,帮助开发者彻底吃透MongoDB关联业务开发。

目录

一、前言

二、MongoDB两种核心建模方式

[2.1 内嵌文档(嵌入式)](#2.1 内嵌文档(嵌入式))

[2.2 引用关联(引用式)](#2.2 引用关联(引用式))

三、一对一关系建模与实战

[3.1 业务场景说明](#3.1 业务场景说明)

[3.2 一对一建模规则](#3.2 一对一建模规则)

[3.3 实战:数据新增](#3.3 实战:数据新增)

[3.4 实战:关联查询](#3.4 实战:关联查询)

[3.5 一对一场景选型总结](#3.5 一对一场景选型总结)

四、一对多关系建模与实战

[4.1 业务场景说明](#4.1 业务场景说明)

[4.2 一对多建模规则](#4.2 一对多建模规则)

[4.3 实战:数据新增](#4.3 实战:数据新增)

[4.4 实战:一对多关联查询(核心)](#4.4 实战:一对多关联查询(核心))

[4.5 特殊场景:小数量一对多内嵌](#4.5 特殊场景:小数量一对多内嵌)

五、多对多关系建模与实战(重点补充)

[5.1 业务场景说明](#5.1 业务场景说明)

[5.2 MongoDB多对多建模方案](#5.2 MongoDB多对多建模方案)

方案A:数组引用(简单、推荐中小数据量)

方案B:中间关联集合(标准、企业级)

[5.3 方案一:数组引用(轻量多对多)](#5.3 方案一:数组引用(轻量多对多))

[5.3.1 数据新增](#5.3.1 数据新增)

[5.3.2 多对多关联查询](#5.3.2 多对多关联查询)

[5.4 方案二:中间集合(企业级标准多对多)](#5.4 方案二:中间集合(企业级标准多对多))

[5.4.1 结构设计](#5.4.1 结构设计)

[5.4.2 数据新增](#5.4.2 数据新增)

[5.4.3 多层联表查询(完整多对多结果)](#5.4.3 多层联表查询(完整多对多结果))

[5.5 多对多方案选型](#5.5 多对多方案选型)

六、三种关系建模选型对照表(全新更新)

七、常见问题总结

[7.1 什么时候不用内嵌?](#7.1 什么时候不用内嵌?)

[7.2 lookup查询为空怎么办?](#7.2 lookup查询为空怎么办?)

[7.3 MongoDB需要手动维护关联关系吗?](#7.3 MongoDB需要手动维护关联关系吗?)

[7.4 多对多开发避坑](#7.4 多对多开发避坑)

八、结尾


一、前言

在关系型数据库(MySQL)中,实体关联依靠主键、外键 实现,规则严格且固定。而MongoDB是文档型数据库,没有外键约束,所有数据以BSON文档形式存储,实体之间的关联关系完全由开发者通过内嵌(嵌入文档) 和**引用(存储ObjectId)**手动建模。

日常业务中99%的关联场景都可归纳为 一对一一对多多对多 三种关系。本文聚焦这三种核心关系,通过真实业务案例+可运行代码,手把手实现数据新增、关联查询,解决新手建模混乱、关联查询不会写的问题。

二、MongoDB两种核心建模方式

在正式讲解关系前,先明确MongoDB唯一的两种关联建模方式,所有一对一、一对多、多对多场景均基于这两种方式实现:

2.1 内嵌文档(嵌入式)

将子文档直接嵌套在父文档内部,数据存储在同一个集合、同一个文档中,无需跨集合查询。

优点:查询效率极高,一次查询即可获取所有关联数据,支持原子操作。

缺点:数据冗余,文档体积会变大,不适合子数据频繁新增、数量不固定的场景。

2.2 引用关联(引用式)

父子数据分属不同集合,父文档中存储子文档的 ObjectId(MongoDB文档唯一主键),通过ID关联两个集合的数据。

优点:数据解耦、无冗余,支持子数据无限扩展,适合动态新增的场景。

缺点:需要跨集合关联查询,相比内嵌方式多一次查询开销。

三、一对一关系建模与实战

3.1 业务场景说明

场景用户 - 用户详情

一个用户对应唯一的个人详情(身份证、性别、年龄、住址),一个详情仅归属一个用户,数据一一对应,且详情数据固定、极少单独查询,完美适配一对一内嵌建模

3.2 一对一建模规则

优先使用内嵌文档,将附属、唯一的子文档直接嵌入主文档,无需拆分集合。

3.3 实战:数据新增

创建 user 集合,将用户详情内嵌到用户文档中,实现一对一数据新增:

bash 复制代码
// 切换数据库
use test_db;

// 一对一新增:用户+唯一用户详情(内嵌文档)
db.user.insertOne({
  username: "张三",
  phone: "13800138000",
  // 内嵌一对一详情文档
  user_info: {
    gender: "男",
    age: 25,
    id_card: "110101199901011234",
    address: "北京市朝阳区"
  }
});

// 批量新增一对一数据
db.user.insertMany([
  {
    username: "李四",
    phone: "13900139000",
    user_info: {
      gender: "女",
      age: 23,
      id_card: "310101200102023456",
      address: "上海市浦东新区"
    }
  },
  {
    username: "王五",
    phone: "13700137000",
    user_info: {
      gender: "男",
      age: 28,
      id_card: "440101199605056789",
      address: "广州市天河区"
    }
  }
]);

3.4 实战:关联查询

一对一内嵌文档查询分为:整体查询、精准匹配内嵌字段查询、指定字段返回查询。

bash 复制代码
// 1. 查询所有用户及内嵌的详情信息
db.user.find();

// 2. 精准查询:查询年龄25岁的用户(匹配内嵌文档字段)
db.user.find({ "user_info.age": 25 });

// 3. 筛选返回字段:只展示用户名、年龄、地址,隐藏_id
db.user.find(
  {},
  { _id: 0, username: 1, "user_info.age": 1, "user_info.address": 1 }
);

// 4. 单条精准查询
db.user.findOne({ username: "张三" });

3.5 一对一场景选型总结

✅ 适用内嵌:数据唯一、固定、极少修改、跟随主数据一起查询(用户-详情、商品-参数)

❌ 不适用内嵌:子数据需单独查询、频繁修改(此类场景可改用引用式一对一)

四、一对多关系建模与实战

4.1 业务场景说明

场景用户 - 订单

一个用户可以创建多个订单,一个订单仅归属一个用户,是最经典的一对多 业务场景。订单数据动态新增、数量不固定,适合引用式建模(拆分双集合,通过用户ID关联)。

4.2 一对多建模规则

主集合(用户)、子集合(订单)拆分存储,子文档中存储主文档的 ObjectId 实现关联,通过 $lookup 实现联表查询。

4.3 实战:数据新增

分两步新增:先新增主数据(用户),再新增子数据(订单)并绑定用户ID:

bash 复制代码
// 1. 新增主数据:用户(主集合 user)
// 保存返回结果,获取用户ObjectId,用于绑定订单
let userRes = db.user.insertOne({
  username: "赵六",
  phone: "13600136000"
});
// 获取新增用户的ID
let userId = userRes.insertedId;

// 2. 新增子数据:订单(子集合 order),通过user_id关联用户
db.order.insertMany([
  {
    order_no: "OD20260708001",
    price: 99.9,
    goods_name: "无线鼠标",
    create_time: new Date(),
    user_id: userId // 关联用户ID,实现一对多绑定
  },
  {
    order_no: "OD20260708002",
    price: 299,
    goods_name: "机械键盘",
    create_time: new Date(),
    user_id: userId
  }
]);

// 批量新增其他用户订单数据
db.order.insertMany([
  {
    order_no: "OD20260708003",
    price: 199,
    goods_name: "蓝牙耳机",
    create_time: new Date(),
    // 绑定上文李四用户的ID(可自行替换对应ObjectId)
    user_id: ObjectId("xxx")
  }
]);

4.4 实战:一对多关联查询(核心)

MongoDB 通过 $lookup 聚合管道实现跨集合一对多关联查询,等价于MySQL的JOIN联表查询,语法固定:

$lookup参数说明

  • from:需要关联的子集合名称
  • localField:主集合的关联字段
  • foreignField:子集合的关联字段
  • as:关联查询结果的别名(数组格式,存储多条子数据)
bash 复制代码
// 关联查询:查询所有用户及其对应的所有订单(一对多完整关联)
db.user.aggregate([
  {
    $lookup: {
      from: "order",        // 关联订单集合
      localField: "_id",    // 用户集合的关联字段(主键ID)
      foreignField: "user_id", // 订单集合的关联字段(外键user_id)
      as: "order_list"      // 订单数据结果存入order_list数组
    }
  }
]);

// 精准查询:查询【赵六】的所有订单信息
db.user.aggregate([
  // 第一步:筛选指定用户
  { $match: { username: "赵六" } },
  // 第二步:关联查询订单
  {
    $lookup: {
      from: "order",
      localField: "_id",
      foreignField: "user_id",
      as: "order_list"
    }
  }
]);

// 筛选过滤:只查询订单金额大于100元的用户订单
db.user.aggregate([
  { $match: { username: "赵六" } },
  {
    $lookup: {
      from: "order",
      localField: "_id",
      foreignField: "user_id",
      as: "order_list"
    }
  },
  // 过滤订单数组数据
  {
    $addFields: {
      order_list: {
        $filter: {
          input: "$order_list",
          as: "item",
          cond: { $gte: ["$$item.price", 100] }
        }
      }
    }
  }
]);

4.5 特殊场景:小数量一对多内嵌

若子数据数量极少、固定不变(如用户常用地址、商品标签),也可使用数组内嵌实现一对多,无需拆分集合,查询效率更高:

bash 复制代码
// 数组内嵌一对多:用户多个常用地址
db.user.insertOne({
  username: "孙七",
  phone: "13500135000",
  // 内嵌多个地址,一对多数组存储
  address_list: [
    { type: "家庭地址", addr: "成都市锦江区" },
    { type: "公司地址", addr: "成都市高新区" }
  ]
});

// 查询内嵌数组地址
db.user.find({ "address_list.type": "家庭地址" });

五、多对多关系建模与实战(重点补充)

5.1 业务场景说明

场景学生 <=> 课程

一名学生可以选修多门课程,一门课程可以被多名学生选择。双向一对多,构成多对多关系

其他常见多对多场景:

1)用户 <=> 角色(一个用户多个角色、一个角色多个用户)

2)商品 <=> 标签(一个商品多标签、一个标签多商品)

3)文章 <=> 分类

5.2 MongoDB多对多建模方案

MongoDB 没有原生多对多,行业标准两种方案:

方案A:数组引用(简单、推荐中小数据量)

一方文档中用数组存储另一方的 ObjectId,结构简单、查询方便。缺点:不适合频繁新增、超大数量关联。

方案B:中间关联集合(标准、企业级)

单独建立一张「中间关联集合」,只存储双方ID,完全对标MySQL多对多中间表。适合大数据量、频繁增删关联关系、需要附带关联字段(选课时间、分数)

5.3 方案一:数组引用(轻量多对多)

5.3.1 数据新增
bash 复制代码
use test_db;

// 1. 创建课程集合
db.course.insertMany([
  { name: "Java开发", teacher: "张老师" },
  { name: "MongoDB数据库", teacher: "李老师" },
  { name: "Python爬虫", teacher: "王老师" }
]);

// 2. 获取课程ID(实际开发可直接写死ID)
let javaId = db.course.findOne({name:"Java开发"})._id;
let mongoId = db.course.findOne({name:"MongoDB数据库"})._id;

// 3. 学生文档用数组引用多门课程(多对多核心)
db.student.insertMany([
  {
    stu_name: "小明",
    age: 20,
    // 选修多门课程
    course_ids: [javaId, mongoId]
  },
  {
    stu_name: "小红",
    age: 19,
    course_ids: [mongoId]
  }
]);
5.3.2 多对多关联查询
bash 复制代码
// 查询学生对应的所有选修课程
db.student.aggregate([
  {
    $lookup:{
      from:"course",
      localField:"course_ids",
      foreignField:"_id",
      as:"course_list"
    }
  }
]);

原理:localField 传入数组时,$lookup 会自动匹配数组中所有ID,天然实现多对多匹配。

5.4 方案二:中间集合(企业级标准多对多)

适用:关联关系频繁变动、数据量大、需要记录关联附加信息(选课时间、成绩、状态)。

5.4.1 结构设计
  • student:学生主集合
  • course:课程主集合
  • stu_course_rel:中间关联集合(存储 stu_id、course_id、score、create_time)
5.4.2 数据新增
bash 复制代码
use test_db;

// 1. 新增学生
let stu = db.student.insertOne({stu_name:"小李",age:21});
let stuId = stu.insertedId;

// 2. 新增课程
let cs = db.course.insertOne({name:"Go语言开发",teacher:"陈老师"});
let csId = cs.insertedId;

// 3. 新增多对多关联关系(中间表)
db.stu_course_rel.insertOne({
  stu_id: stuId,
  course_id: csId,
  score: 0,
  select_time: new Date()
});
5.4.3 多层联表查询(完整多对多结果)
bash 复制代码
// 查询所有学生 + 所选课程 + 选课信息
db.student.aggregate([
  // 第一步:关联中间表
  {
    $lookup: {
      from: "stu_course_rel",
      localField: "_id",
      foreignField: "stu_id",
      as: "rel_list"
    }
  },
  // 第二步:拆分数组,扁平化数据
  { $unwind: "$rel_list" },
  // 第三步:关联真实课程数据
  {
    $lookup: {
      from: "course",
      localField: "rel_list.course_id",
      foreignField: "_id",
      as: "course_info"
    }
  },
  { $unwind: "$course_info" }
]);

5.5 多对多方案选型

数组引用 :简单、查询快、适合标签、权限角色、固定关联场景

中间集合:解耦、可扩展、可加字段、适合选课、订单商品、复杂权限系统

六、三种关系建模选型对照表(全新更新)

|-------------|--------|---------------------------|-----------------|-----------------------|
| 关系类型 | 建模方式 | 适用场景 | 优点 | 缺点 |
| 一对一 | 内嵌文档 | 数据唯一、固定、跟随主数据查询,极少单独操作子数据 | 查询快、无跨集合操作、原子性强 | 轻微数据冗余,子数据无法独立查询 |
| 一对多(少量子数据) | 数组内嵌 | 子数据数量固定、不持续新增,如标签、地址 | 无需联表,一次查询获取全部数据 | 子数据过多会导致文档臃肿 |
| 一对多(大量子数据) | ID引用关联 | 子数据动态新增、数量不固定,如订单、评论 | 数据解耦、无冗余、扩展性强 | 需要$lookup联表查询,有轻微性能开销 |
| 多对多(简单) | ID数组引用 | 关联关系稳定、不频繁增减,如角色权限、商品标签 | 结构简单、查询高效、无需中间表 | 无法存储关联附加字段,频繁变更性能差 |
| 多对多(复杂/企业级) | 中间集合关联 | 关联关系频繁变动、需要附加字段(时间、分数、状态) | 高度解耦、可扩展、适配大数据量 | 查询层级多,需要多次lookup |

七、常见问题总结

7.1 什么时候不用内嵌?

子数据需要单独增删改查、子数据数量无限增长、主文档频繁更新时,禁止内嵌,优先使用引用关联。

7.2 $lookup查询为空怎么办?

  1. 检查关联字段类型是否一致(常见错误:主键是ObjectId,外键存的是字符串);2. 检查字段名是否拼写错误;3. 确认子数据已绑定对应主ID。

7.3 MongoDB需要手动维护关联关系吗?

需要!MongoDB无外键约束,删除主数据后,子数据不会自动删除,业务代码中需手动处理关联数据的同步删除、更新。

7.4 多对多开发避坑

1)数组引用不适合上万条关联数据,会造成单文档过大;

2)需要排序、分页、筛选关联关系时,优先使用中间集合;

3)多对多聚合查询记得搭配 $unwind 扁平化数组,否则数据层级嵌套过深。

八、结尾

MongoDB的文档关系建模核心是场景优先 :一对一、静态少量一对多优先内嵌;动态海量一对多优先ID引用;简单多对多用数组引用,复杂多对多用中间集合。掌握 内嵌文档$lookup关联查询中间集合建模,即可覆盖99%的日常业务关联场景。

相关推荐
卓怡学长1 小时前
w268基于springboot + vue 物流系统
java·数据库·vue.js·spring boot·spring·intellij-idea
.Cnn2 小时前
Redis基础day02
java·数据库·redis
能有时光2 小时前
[MAF的Agent管道详解-07]利用AIAgent中间件构建Agent管道
java·数据库
迷枫7122 小时前
简单sql与dm功能实验记录
数据库·sql·oracle
蚁库2 小时前
Oracle 19c 文件系统安装实战教程:从规划到日常管理完整流程
数据库·oracle
枫叶丹42 小时前
Loop Engineering:从提示词到循环系统
数据库·redis·缓存·loop
zhanghaofaowhrql3 小时前
Spring Data JPA Repository 详解:从基础到高级用法
java·数据库·sql
techdashen4 小时前
Uber 如何让 MySQL Group Replication 在大规模生产环境中可用:自动化、故障转移与一致性保障
数据库·mysql·自动化
葫芦和十三11 小时前
图解 MongoDB 30|查询路由与广播查询:带片键定向,不带就广播
后端·mongodb·agent