mongoose基础学习之增删改查(1)

一、Mongoose 学习笔记1(增删改查)

1. Mongoose 是什么

Mongoose 是一个 Node.js 的 ODM(对象文档映射)库,用于在 Node.js 环境中操作 MongoDB 数据库。

  • MongoDB 是非关系型数据库(NoSQL),而 Mongoose 提供了类似关系型数据库的操作方式

  • 通过 Schema(模式) 定义数据结构,使非关系型数据库操作更加结构化、规范化

  • 提供了类型转换、验证、查询构建、中间件等功能

💡 简单理解:Mongoose 就像一座桥梁,让开发者可以用熟悉的方式(类似 SQL)来操作 MongoDB

2. 安装以及使用

2.1 安装

javascript 复制代码
npm install mongoose

2.2 基本使用流程

(1)引入 Mongoose

javascript 复制代码
const mongoose = require('mongoose');

(2)连接数据库

javascript 复制代码
// 不加密的连接(无用户名密码)
mongoose.connect("mongodb://localhost:27017/数据库名");

// 加密的连接(有用户名密码)
mongoose.connect("mongodb://用户名:密码@localhost:27017/数据库名");

// 使用选项(推荐写法)
mongoose.connect("mongodb://localhost:27017/myapp", {
  useNewUrlParser: true,
  useUnifiedTopology: true
});

(3)监听连接状态(可选)

javascript 复制代码
mongoose.connection.on('connected', () => {
  console.log('数据库连接成功');
});

mongoose.connection.on('error', (err) => {
  console.log('数据库连接失败', err);
});

3.Schema(模式)

3.1模式修饰符Getters与setter自定义 修饰符

除了 Mongoose 内置的修饰符(如 trim、lowercase 等),我们还可以通过自定义 set 和 get 来对数据进行格式化处理。

  • set:在写入数据时对字段进行格式化(推荐使用)。
  • get:在读取数据时对字段进行格式化(注意:不会修改数据库中的实际值,仅在实例获取时生效,一般不推荐用于关键业务逻辑)。
javascript 复制代码
const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
    trim: true,                // 内置修饰符:去除前后空格
    get(params) {              // 读取时加前缀,不存入数据库
      return '001' + params;
    }
  },
  pic: {
    type: String,
    set(params) {              // 写入时处理,自动补全 http/https
      if (!params) {
        return params;
      } else {
        if (params.indexOf('http://') !== 0 && params.indexOf('https://') !== 0) {
          return 'http://' + params;
        }
        return params;
      }
    }
  }
});

⚠️ 注意:get 仅在通过模型实例读取字段时生效,不会影响数据库存储内容。

3.2mongoose设置索引

索引可以提高查询速度,但会降低写入性能,因此需要根据实际查询场景合理设计。

javascript 复制代码
const userSchema = new mongoose.Schema({
  sn: {
    type: String,
    unique: true    // 唯一索引
  },
  name: {
    type: String,
    index: true     // 普通索引
  }
});
// ⚠️ 注意:这样创建的是两个独立的单字段索引,不是复合索引

复合索引(Compound Index)是指在多个字段上联合建立索引,用于优化涉及多个字段的查询条件。
1. 基本语法

在 Mongoose 中,复合索引有两种定义方式:

方式一:在 Schema 定义时添加

javascript 复制代码
const userSchema = new mongoose.Schema({
  name: { type: String },
  age: { type: Number },
  email: { type: String },
  createTime: { type: Date }
});

// 定义复合索引:name 升序 + age 降序
userSchema.index({ name: 1, age: -1 });
// 1 表示升序(从小到大)
// -1 表示降序(从大到小)


// 定义复合唯一索引:name 和 email 组合唯一
userSchema.index({ name: 1, email: 1 }, { unique: true });
// 允许以下插入:
// { name: "张三", email: "zhangsan@example.com" }
// { name: "张三", email: "zhangsan2@example.com" } ✅ 不同邮箱
// { name: "李四", email: "zhangsan@example.com" } ✅ 不同名字

// 不允许以下插入(组合重复):
// { name: "张三", email: "zhangsan@example.com" } ❌ 已存在相同组合
javascript 复制代码
userSchema.index(
  { name: 1, age: -1 },
  {
    unique: true,           // 唯一索引
    name: 'name_age_index', // 自定义索引名称
    sparse: true,           // 稀疏索引(忽略缺失字段的文档)
    background: true,       // 后台创建索引(不阻塞数据库操作)
    expireAfterSeconds: 3600 // TTL索引(仅对日期字段有效,需注意复合索引的限制)
  }
);

复合索引的注意事项

javascript 复制代码
// ❌ 错误示例:索引过多且冗余
userSchema.index({ name: 1 });
userSchema.index({ name: 1, age: 1 });
userSchema.index({ name: 1, age: 1, email: 1 });
// 实际上,最后一个索引可以覆盖前两个的查询场景

// ✅ 优化:只保留最全面的复合索引
userSchema.index({ name: 1, age: 1, email: 1 });

复合索引是 MongoDB 性能优化的核心工具,合理使用可以大幅提升查询效率,但需要根据实际查询场景权衡索引字段的顺序和数量。

索引类型 说明
unique: true 字段值唯一,自动创建唯一索引
index: true 创建普通索引,提升查询效率
复合索引 可在 Schema 外通过 schema.index({ field1: 1, field2: -1 }) 定义
3.2.1unique 唯一索引

📚 基本概念

  1. unique: true - 唯一约束
    确保字段值在集合中唯一,不允许重复。
javascript 复制代码
userSchema.index({ phone: 1 }, { unique: true });
// 效果:
await User.create({ phone: '13800138000' });  // ✅ 成功
await User.create({ phone: '13800138000' });  // ❌ 报错:重复键
3.2.2 sparse稀疏索引

只索引包含该字段的文档,忽略字段不存在的文档。

javascript 复制代码
userSchema.index({ phone: 1 }, { sparse: true });
// 效果:
await User.create({ name: '张三' });          
await User.create({ name: '李四', phone: '13800138000' }); 
await User.create({ name: '王五', phone: '13800138000' }); 
3.2.3组合使用:{ unique: true, sparse: true }

为什么需要组合?

这是处理可选唯一字段的最佳实践!

javascript 复制代码
userSchema.index({ phone: 1 }, { unique: true, sparse: true });

场景:手机号可选但唯一

javascript 复制代码
const userSchema = new mongoose.Schema({
  name: String,
  phone: { type: String, required: false },  // 手机号可选
  email: String
});

// 创建索引:手机号可选但唯一
userSchema.index({ phone: 1 }, { unique: true, sparse: true });

// 测试:
// ✅ 可以创建多个没有手机号的用户
await User.create({ name: '张三' });           // 成功
await User.create({ name: '李四' });           // 成功

// ✅ 可以创建有手机号的用户
await User.create({ name: '王五', phone: '13800138000' });  // 成功

// ❌ 不能创建重复手机号的用户
await User.create({ name: '赵六', phone: '13800138000' });  // 报错!手机号重复

// ✅ 同一个手机号只能出现一次,但可以有多个 null/undefined

⚠️ 如果不加 sparse: true 会怎样?

javascript 复制代码
// 只有 unique,没有 sparse
userSchema.index({ phone: 1 }, { unique: true });

// 问题:MongoDB 将 null 也视为一个值
await User.create({ name: '张三' });           // phone 为 undefined/null
await User.create({ name: '李四' });           // ❌ 报错!重复的 null 值
// 错误信息:E11000 duplicate key error collection
// 结果:只能有一个没有手机号的用户!

📊 对比表格

配置 允许重复 null 允许重复值 索引大小 适用场景
无索引 不常用
unique: true 全字段 必填唯一字段(如主键)
sparse: true 可选字段的快速查询
unique: true, sparse: true 可选但唯一的字段 ✅
3.2.4🎯 实际应用场景

场景1:用户系统的手机号/邮箱

javascript 复制代码
const userSchema = new mongoose.Schema({
  username: { type: String, required: true, unique: true },  // 必填唯一
  phone: { type: String, sparse: true, unique: true },       // 可选唯一
  email: { type: String, sparse: true, unique: true },       // 可选唯一
  wechatId: { type: String, sparse: true, unique: true }     // 可选唯一
});

// 索引
userSchema.index({ phone: 1 }, { unique: true, sparse: true });
userSchema.index({ email: 1 }, { unique: true, sparse: true });
userSchema.index({ wechatId: 1 }, { unique: true, sparse: true });

// 使用效果:
// ✅ 可以注册多个只填用户名的账号
await User.create({ username: 'user1' });
await User.create({ username: 'user2' });

// ✅ 可以绑定手机号(唯一)
await User.findByIdAndUpdate(user1Id, { phone: '13800138000' });

// ❌ 不能绑定已被使用的手机号
await User.findByIdAndUpdate(user2Id, { phone: '13800138000' }); // 报错

// ✅ 可以解绑手机号(设为 null)
await User.findByIdAndUpdate(user1Id, { phone: null }); // 成功

// ✅ 其他用户可以重新绑定这个手机号
await User.findByIdAndUpdate(user2Id, { phone: '13800138000' }); // 成功

场景2:第三方账号绑定

javascript 复制代码
const userSchema = new mongoose.Schema({
  local: {
    email: String,
    password: String
  },
  google: {
    id: { type: String, sparse: true, unique: true },
    email: String
  },
  facebook: {
    id: { type: String, sparse: true, unique: true },
    email: String
  }
});

// 索引
userSchema.index({ 'google.id': 1 }, { unique: true, sparse: true });
userSchema.index({ 'facebook.id': 1 }, { unique: true, sparse: true });

// 效果:
// ✅ 可以有多个没有绑定 Google 的用户
// ✅ 每个 Google 账号只能绑定一个用户
// ✅ 解绑后可以重新绑定给其他用户

3.3扩展查询

3.3.1静态方法

静态方法定义在 Model 上,通过 Model 直接调用,适用于不依赖具体实例的查询。

javascript 复制代码
const userSchema = new mongoose.Schema({
  sn: { type: String, unique: true },
  name: { type: String, index: true }
});

// 定义静态方法
userSchema.statics.findBySn = function (sn, cb) {
  // this 指向当前 Model
  this.find({ sn: sn }, function (err, docs) {
    cb(err, docs);
  });
};

// 使用
const User = mongoose.model('User', userSchema);
User.findBySn('1222', (err, docs) => {
  console.log(docs);
});
3.3.2 实例方法(Methods)

实例方法定义在 文档实例 上,通过具体实例调用,适用于操作当前文档数据。

javascript 复制代码
userSchema.methods.findBySn = function (sn, cb) {
  // this 指向当前文档实例
  this.model('User').find({ sn: sn }, function (err, docs) {
    cb(err, docs);
  });
};

// 使用
const user = new User({ sn: '1222', name: 'Alice' });
user.findBySn('1222', (err, docs) => {
  console.log(docs);
});

✅ 静态方法与实例方法的区别:

静态方法:通过 Model 调用,常用于通用查询。

实例方法:通过具体文档调用,常用于操作当前文档或与其相关的逻辑。

3.4 Schema 与 Model 的关系

  • Schema:定义数据结构(字段、类型、校验、索引等),不操作数据库。
  • Model:通过 mongoose.model() 编译 Schema 生成,具备数据库操作能力(如 find、create、update 等)。
javascript 复制代码
const userSchema = new mongoose.Schema({
  username: String,
  age: Number
});

// 生成 Model
const User = mongoose.model('User', userSchema);

// 通过 Model 操作数据库
User.create({ username: 'Alice', age: 18 });

// Schema 支持的数据类型:
// String, Number, Date, Boolean, Array, ObjectId, Mixed 等

⚠️ 重要:Schema 本身不具备操作数据库的能力,它只是定义了数据结构。要操作数据库,必须通过 Model(模型)。

3.5 数据校验

Mongoose 提供了丰富的内置校验规则,可以在 Schema 定义时对数据进行验证,确保数据的完整性和一致性。校验在调用 save()、create() 等方法时自动执行。

3.5.1.常用内置校验规则
javascript 复制代码
const userSchema = new mongoose.Schema({
  // 字符串类型校验
  username: {
    type: String,
    required: [true, '用户名是必填字段'],  // 必填校验,自定义错误信息
    trim: true,                           // 去除首尾空格(内置修饰符)
    minlength: [2, '用户名长度不能少于2个字符'],  // 最小长度
    maxlength: [10, '用户名长度不能超过10个字符'], // 最大长度
    match: [/^[a-zA-Z0-9]+$/, '用户名只能包含字母和数字'], // 正则匹配
    lowercase: true,                      // 转为小写(内置修饰符)
    uppercase: false                      // 转为大写(内置修饰符)
  },
  
  // 数字类型校验
  age: {
    type: Number,
    required: [true, '年龄是必填字段'],
    min: [0, '年龄不能小于0岁'],           // 最小值
    max: [150, '年龄不能超过150岁'],        // 最大值
    validate: {                           // 自定义校验器
      validator: function(v) {
        return Number.isInteger(v);       // 必须是整数
      },
      message: '年龄必须是整数'
    }
  },
  
  // 密码字段
  password: {
    type: String,
    required: [true, '密码是必填字段'],
    minlength: [6, '密码长度不能少于6位'],
    maxlength: [20, '密码长度不能超过20位'],
    match: [/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, '密码必须包含大小写字母和数字']
  },
  
  // 枚举校验(只能是指定值之一)
  status: {
    type: String,
    default: 'success',
    enum: {
      values: ['success', 'error', 'warning'],
      message: '状态值必须是 success、error 或 warning'
    }
  },
  
  // 邮箱校验
  email: {
    type: String,
    required: true,
    unique: true,                         // 唯一索引(不是校验,但会报错)
    validate: {
      validator: function(v) {
        return /^[^\s@]+@([^\s@]+\.)+[^\s@]+$/.test(v);
      },
      message: '邮箱格式不正确'
    }
  },
  
  // 数组类型校验
  tags: {
    type: [String],
    validate: {
      validator: function(v) {
        return v.length <= 5;             // 标签不超过5个
      },
      message: '标签数量不能超过5个'
    }
  }
});
3.5.2自定义校验器

当内置校验规则不满足需求时,可以使用自定义校验器:

javascript 复制代码
const orderSchema = new mongoose.Schema({
  totalAmount: {
    type: Number,
    required: true,
    min: 0
  },
  discount: {
    type: Number,
    default: 0,
    validate: {
      validator: function(v) {
        // 校验折扣不能大于总金额
        return v <= this.totalAmount;
      },
      message: '折扣金额不能大于订单总金额'
    }
  },
  items: {
    type: [{
      productId: String,
      quantity: Number,
      price: Number
    }],
    validate: {
      validator: function(items) {
        // 至少有一个商品
        return items && items.length > 0;
      },
      message: '订单至少包含一个商品'
    }
  }
});
3.5.3 异步校验器

适用于需要查询数据库或调用外部服务的场景:

javascript 复制代码
const userSchema = new mongoose.Schema({
  phone: {
    type: String,
    validate: {
      validator: async function(phone) {
        // 检查手机号是否已被注册
        const existingUser = await this.constructor.findOne({ phone });
        return !existingUser;
      },
      message: '手机号已被注册'
    }
  }
});
3.5.4 常用校验选项汇总
校验器 适用类型 说明
required 所有类型 必填字段
minlength / maxlength String 字符串长度限制
min / max Number, Date 数值或日期范围限制
match String
enum String, Number 枚举值限制
validate 所有类型 自定义校验器
unique 所有类型 唯一索引 (非校验,会抛出重复错误)

3.6Mongoose 虚拟字段

什么是虚拟字段

虚拟字段是不在数据库中存储,但在模型实例上可以访问的字段。它通过 get/set 动态计算值。

3.6.1 基础虚拟字段
  1. 基础 Getter(获取值)
javascript 复制代码
const userSchema = new mongoose.Schema({
  firstName: String,
  lastName: String,
  birthDate: Date
});

// 定义虚拟字段
userSchema.virtual('fullName').get(function() {
  return `${this.firstName} ${this.lastName}`;
});

userSchema.virtual('age').get(function() {
  if (!this.birthDate) return null;
  const today = new Date();
  const birthDate = new Date(this.birthDate);
  let age = today.getFullYear() - birthDate.getFullYear();
  const monthDiff = today.getMonth() - birthDate.getMonth();
  if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
    age--;
  }
  return age;
});

const User = mongoose.model('User', userSchema);
  1. Set(设置值)
javascript 复制代码
userSchema.virtual('fullName').set(function(value) {
  const parts = value.split(' ');
  this.firstName = parts[0];
  this.lastName = parts[1];
});

// 使用 setter
const user = new User();
user.fullName = 'John Doe'; // 自动拆分到 firstName 和 lastName
console.log(user.firstName); // 'John'
console.log(user.lastName);  // 'Doe'
3.6.2 虚拟字段的使用方式

1.基础方法

javascript 复制代码
const user = await User.findOne({ firstName: 'John' });

// ✅ 直接访问虚拟字段
console.log(user.fullName); // 'John Doe'
console.log(user.age);      // 25(如果设置了 birthDate)

// ❌ 虚拟字段不在数据库文档中
console.log(user.toObject()); // 默认不包含虚拟字段
  1. 在查询结果中包含虚拟字段
    方法一:使用 toJSON 或 toObject 配置
javascript 复制代码
// Schema 级别配置
const userSchema = new mongoose.Schema({
  firstName: String,
  lastName: String
}, {
  toJSON: { virtuals: true },  // 将虚拟字段包含在 JSON 输出中
  toObject: { virtuals: true }  // 将虚拟字段包含在对象输出中
});

userSchema.virtual('fullName').get(function() {
  return `${this.firstName} ${this.lastName}`;
});

// 使用时
const user = await User.findOne();
console.log(user.toJSON());  // 包含 fullName
console.log(user.toObject()); // 包含 fullName

// res.json() 会自动调用 toJSON
res.json(user); // 前端会收到 fullName 字段

方法二:临时获取

javascript 复制代码
const user = await User.findOne();

// 临时转换为包含虚拟字段的对象
const userWithVirtuals = user.toObject({ virtuals: true });
console.log(userWithVirtuals.fullName);

// 或者直接访问
console.log(user.fullName); // 直接访问虚拟属性
3.6.33. 关联虚拟字段的使用(需要 populate)
javascript 复制代码
// Schema 定义
const doctorSchema = new mongoose.Schema({
  name: String
});

doctorSchema.virtual('patientCount', {
  ref: 'User',
  localField: '_id',
  foreignField: 'doctorId',
  count: true
});

doctorSchema.virtual('patients', {
  ref: 'User',
  localField: '_id',
  foreignField: 'doctorId',
  justOne: false
});

const Doctor = mongoose.model('Doctor', doctorSchema);

// 使用虚拟字段必须 populate
// ✅ 正确使用
const doctor = await Doctor.findById(doctorId)
  .populate('patientCount')  // 获取数量
  .populate('patients', 'name age'); // 获取患者列表

console.log(doctor.patientCount); // 5
console.log(doctor.patients);     // [{ name: '张三', age: 25 }, ...]

// ❌ 错误使用(不会自动填充)
const doctor2 = await Doctor.findById(doctorId);
console.log(doctor2.patientCount); // undefined(除非配置了 toJSON/toObject)
3.5.5前置钩子

一、什么是前置钩子

前置钩子是在数据库操作执行之前自动触发的中间件函数,用于在保存、更新、删除等操作前执行自定义逻辑。

核心概念图

xml 复制代码
调用操作 (save/update/remove)
    ↓
触发前置钩子 ← 在这里插入自定义逻辑
    ↓
执行实际数据库操作
    ↓
触发后置钩子
    ↓
返回结果

4. Model(模型)

Model 是由 Schema 编译生成的构造函数,它代表了数据库中的集合,并提供了操作数据库的所有方法。

javascript 复制代码
// 定义模型
// 参数1:模型名称(首字母通常大写,单数形式)
// 参数2:对应的 Schema
// 返回值:模型对象
const User = mongoose.model('User', userSchema);//映射到users表
// const User = mongoose.model('User', userSchema,user);// 映射到user表

模型名称与集合名称的对应规则:

Mongoose 会将模型名称 自动转为小写 并 变为复数 作为集合名

例如:User → users 集合

例如:Article → articles 集合

javascript 复制代码
// 这行代码会操作数据库中名为 "users" 的集合
const User = mongoose.model('User', userSchema);

5. 模块化导出

将模型单独封装,便于在其他文件中使用:

model/user.js

javascript 复制代码
const mongoose = require('mongoose');

// 定义 Schema
const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true //必填字段
  },
  age: {
    type: Number,
    required: true
  },
  email: String,//非必填字段 但是必须是字符串类型
  sex:{
  		type:Number,
  		defalut:1//默认值为1 
  }
});

// 创建模型
const User = mongoose.model('User', userSchema);

// ✅ 正确写法:导出模型
module.exports = User;  // 注意是 module,不是 model

app.js(使用模型的文件)

javascript 复制代码
// 引入模型
const User = require('./model/user.js');

// 创建文档实例
const user = new User({
  username: '张三',
  age: 25,
  email: 'zhangsan@example.com'
});

// 保存到数据库
user.save()
  .then(doc => console.log('保存成功', doc))
  .catch(err => console.log('保存失败', err));

// 或使用 async/await
async function createUser() {
  try {
    const user = await User.create({
      username: '李四',
      age: 30
    });
    console.log('创建成功', user);
  } catch (err) {
    console.log('创建失败', err);
  }
}

6常用数据库操作

6.1查询操作find

javascript 复制代码
// 查询所有
const users = await User.find({});
// 条件查询
await =User.find({ age: { $gte: 18 } });
// 查询单条
await =User.findOne({ username: '张三' });
// 根据 ID 查询
await = User.findById('507f1f77bcf86cd799439011');

6.2添加操作create

javascript 复制代码
// 方式1:实例化 + save
const user = new User({ username: '王五', age: 28 });
await user.save();
// 方式2:create 方法(推荐)
const user = await User.create({ username: '赵六', age: 32 });

6.3修改操作update

javascript 复制代码
// 更新单条
await User.updateOne({ username: '张三' }, { age: 26 });
// 更新多条
await User.updateMany({ age: 25 }, { age: 26 });
// 查找并更新
await User.findOneAndUpdate({ username: '李四' }, { age: 31 }, { new: true });

6.4删除操作 delete

javascript 复制代码
// 删除单条
await User.deleteOne({ username: '张三' });
// 删除多条
await User.deleteMany({ age: { $lt: 18 } });
// 查找并删除
await User.findOneAndDelete({ username: '王五' });

7. 数据聚合

Mongoose 的聚合管道是 MongoDB 中最强大的数据处理工具,它允许你在数据库层面执行复杂的数据分析、转换和关联操作。我把它系统地整理了一下,希望能帮你更好地学习和使用。

7.1 核心概念:什么是聚合管道?

可以把聚合管道想象成一个工厂的流水线。原始数据(文档)从一端进入,依次经过多个加工站(Stage)。每个加工站都对数据进行一种特定的处理(如筛选、分组、关联等),最终在流水线的末端产出我们想要的结果。

在 Mongoose 中,使用 Model.aggregate() 方法来启动这个管道,它接收一个由多个阶段对象组成的数组作为参数

7.1.1 单张表的聚合

User 集合的数据格式

javascript 复制代码
//user 用户表的信息
[
  {
    "_id": ObjectId("507f1f77bcf86cd799439011"),
    "name": "张三",
    "age": 25,
    "city": "北京",        // 注意:这是一个字符串字段,不是外键
    "email": "zhangsan@example.com",
    "createdAt": ISODate("2024-01-01T00:00:00Z")
  },
  {
    "_id": ObjectId("507f1f77bcf86cd799439012"),
    "name": "李四",
    "age": 32,
    "city": "上海",
    "email": "lisi@example.com",
    "createdAt": ISODate("2024-01-02T00:00:00Z")
  },
  {
    "_id": ObjectId("507f1f77bcf86cd799439013"),
    "name": "王五",
    "age": 19,
    "city": "北京",
    "email": "wangwu@example.com",
    "createdAt": ISODate("2024-01-03T00:00:00Z")
  },
  {
    "_id": ObjectId("507f1f77bcf86cd799439014"),
    "name": "赵六",
    "age": 16,           // 这个会被 $match 过滤掉(年龄<18)
    "city": "深圳",
    "email": "zhaoliu@example.com",
    "createdAt": ISODate("2024-01-04T00:00:00Z")
  },
  {
    "_id": ObjectId("507f1f77bcf86cd799439015"),
    "name": "周七",
    "age": 28,
    "city": "上海",
    "email": "zhouqi@example.com",
    "createdAt": ISODate("2024-01-05T00:00:00Z")
  }
]

聚合

javascript 复制代码
const User = mongoose.model('User', userSchema);

User.aggregate([
  { $match: { age: { $gte: 18 } } },    // 阶段1: 筛选出成年人
  { $group: { _id: '$city', count: { $sum: 1 } } } // 阶段2: 按城市分组并计数
])
.then(result => console.log(result)); 

聚合过程详解
第一步

javascript 复制代码
{ $match: { age: { $gte: 18 } } }
// 过滤后的中间结果(赵六被过滤掉)
[
  { name: "张三", age: 25, city: "北京" ,...},
  { name: "李四", age: 32, city: "上海" ,...},
  { name: "王五", age: 19, city: "北京" ,...},
  { name: "周七", age: 28, city: "上海",... }
]

第二步

javascript 复制代码
 $group: { _id: '$city', count: { $sum: 1 } } }
// 按 city 字段的值进行分组,统计每组的人数
// $city 中的 $ 表示引用当前文档的 city 字段

最终输出结果

javascript 复制代码
[
  { "_id": "北京", "count": 2 },  // 张三和王五
  { "_id": "上海", "count": 2 }   // 李四周和周七
]

聚合之后 我想查看之前的全部信息 我应该如何操作呢

javascript 复制代码
const User = mongoose.model('User', userSchema);

User.aggregate([
  { $match: { age: { $gte: 18 } } },    // 阶段1: 筛选出成年人
  { $group: { 
  		_id: '$city', 
  		count: { $sum: 1 } },
  		 users: { $push: { name: '$name', email: '$email' } },  // 保留用户列表
      avgAge: { $avg: '$age' }  // 计算平均年龄
  		} // 阶段2: 按城市分组并计数
])
.then(result => console.log(result)); 
// 输出结果:
[
  {
    "_id": "北京",
    "count": 2,
    "users": [
      { "name": "张三", "email": "zhangsan@example.com" },
      { "name": "王五", "email": "wangwu@example.com" }
    ],
    "avgAge": 22  // (25+19)/2
  },
  {
    "_id": "上海",
    "count": 2,
    "users": [
      { "name": "李四", "email": "lisi@example.com" },
      { "name": "周七", "email": "zhouqi@example.com" }
    ],
    "avgAge": 30  // (32+28)/2
  }
]

📝 总结

$match 保留所有字段:我的简化展示只是教学需要,实际所有字段都在

$group 会重塑数据:只输出你明确指定的字段,其他字段会丢失

7.1.2 两张表的聚合

用户订单列表的聚合

javascript 复制代码
// ========== 表1:users 集合 ==========
[
  {
    "_id": ObjectId("user_001"),
    "name": "张三",
    "age": 25,
    "city": "北京",
    "email": "zhangsan@example.com"
  },
  {
    "_id": ObjectId("user_002"),
    "name": "李四",
    "age": 32,
    "city": "上海",
    "email": "lisi@example.com"
  },
  {
    "_id": ObjectId("user_003"),
    "name": "王五",
    "age": 19,
    "city": "北京",
    "email": "wangwu@example.com"
  }
]

// ========== 表2:orders 集合 ==========
[
  {
    "_id": ObjectId("order_001"),
    "userId": ObjectId("user_001"),  // 外键,指向 users 表
    "orderNo": "ORD001",
    "totalAmount": 299.00,
    "status": "completed",
    "createdAt": ISODate("2024-01-15")
  },
  {
    "_id": ObjectId("order_002"),
    "userId": ObjectId("user_001"),
    "orderNo": "ORD002",
    "totalAmount": 599.00,
    "status": "completed",
    "createdAt": ISODate("2024-01-20")
  },
  {
    "_id": ObjectId("order_003"),
    "userId": ObjectId("user_002"),
    "orderNo": "ORD003",
    "totalAmount": 899.00,
    "status": "pending",
    "createdAt": ISODate("2024-01-25")
  },
  {
    "_id": ObjectId("order_004"),
    "userId": ObjectId("user_002"),
    "orderNo": "ORD004",
    "totalAmount": 1299.00,
    "status": "completed",
    "createdAt": ISODate("2024-02-01")
  }
]

📚 常见关联查询场景

场景1:基础关联 - 获取用户及其订单

javascript 复制代码
User.aggregate([
  {
    $lookup: {
      from: "orders",           // 要关联的集合名
      localField: "_id",        // users 表中的字段
      foreignField: "userId",   // orders 表中的字段
      as: "orders"              // 结果存入的字段名(会是数组)
    }
  }
])

// 输出结果:
[
  {
    "_id": ObjectId("user_001"),
    "name": "张三",
    "age": 25,
    "email": "zhangsan@example.com",
    "orders": [                  // 关联到的订单数组
      { "_id": "order_001", "orderNo": "ORD001", "totalAmount": 299.00, ... },
      { "_id": "order_002", "orderNo": "ORD002", "totalAmount": 599.00, ... }
    ]
  },
  {
    "_id": ObjectId("user_002"),
    "name": "李四",
    "age": 32,
    "orders": [
      { "_id": "order_003", "orderNo": "ORD003", "totalAmount": 899.00, ... },
      { "_id": "order_004", "orderNo": "ORD004", "totalAmount": 1299.00, ... }
    ]
  },
  {
    "_id": ObjectId("user_003"),
    "name": "王五",
    "age": 19,
    "orders": []                 // 没有订单的用户,orders 为空数组
  }
]

场景2:展开订单数组(使用 $unwind)

javascript 复制代码
User.aggregate([
  {
    $lookup: {
      from: "orders",
      localField: "_id",
      foreignField: "userId",
      as: "orders"
    }
  },
  { $unwind: "$orders" }  // 展开数组,每个订单生成一条记录
])

// 输出结果(每个订单一条记录):
[
  {
    "_id": ObjectId("user_001"),
    "name": "张三",
    "age": 25,
    "orders": { "_id": "order_001", "orderNo": "ORD001", "totalAmount": 299.00 }  // 注意:现在是对象,不是数组
  },
  {
    "_id": ObjectId("user_001"),
    "name": "张三",
    "age": 25,
    "orders": { "_id": "order_002", "orderNo": "ORD002", "totalAmount": 599.00 }
  },
  {
    "_id": ObjectId("user_002"),
    "name": "李四",
    "age": 32,
    "orders": { "_id": "order_003", "orderNo": "ORD003", "totalAmount": 899.00 }
  },
  // ... 李四的第二条订单
]

场景2:

需求:查询每个用户,但只关联"已完成"状态的订单

javascript 复制代码
// 需求:查询用户及其已完成状态的订单
User.aggregate([
  {
    $lookup: {
      from: "orders",
      let: { userId: "$_id" },           // 定义变量
      pipeline: [                         // 子管道,可以对关联表进行筛选
        {
          $match: {
            $expr: {
              $and: [
                { $eq: ["$userId", "$$userId"] },  // 匹配用户ID
                { $eq: ["$status", "completed"] }  // 只取已完成的订单
              ]
            }
          }
        },
        { $sort: { createdAt: -1 } },     // 按时间倒序
        { $limit: 5 }                     // 只取最近5条
      ],
      as: "completedOrders"
    }
  }
])

// 输出结果:
[
  {
    "_id": ObjectId("user_001"),
    "name": "张三",
    "completedOrders": [
      { "orderNo": "ORD002", "totalAmount": 599.00, "status": "completed" },  // 已完成
      { "orderNo": "ORD001", "totalAmount": 299.00, "status": "completed" }
    ]
    // ORD003 是 pending 状态,不会被关联进来
  },
  // ...
]

7.1.3多表关联(三张表)

javascript 复制代码
// 新增表3:products 集合
[
  { "_id": ObjectId("prod_001"), "name": "笔记本电脑", "price": 5999 },
  { "_id": ObjectId("prod_002"), "name": "手机", "price": 3999 }
]

// 订单详情表:order_items
[
  { "orderId": "order_001", "productId": "prod_001", "quantity": 1 },
  { "orderId": "order_001", "productId": "prod_002", "quantity": 2 }
]

// 需求:查询订单,包含用户信息和商品信息
Order.aggregate([
  // 第一步:关联用户
  {
    $lookup: {
      from: "users",
      localField: "userId",
      foreignField: "_id",
      as: "user"
    }
  },
  { $unwind: "$user" },
  
  // 第二步:关联订单项
  {
    $lookup: {
      from: "order_items",
      localField: "_id",
      foreignField: "orderId",
      as: "items"
    }
  },
  
  // 第三步:关联商品(通过订单项中的 productId)
  {
    $lookup: {
      from: "products",
      let: { productIds: "$items.productId" },
      pipeline: [
        {
          $match: {
            $expr: { $in: ["$_id", "$$productIds"] }
          }
        }
      ],
      as: "products"
    }
  }
])
相关推荐
深蓝海拓2 小时前
西门子S7-1500PLC的PEEK/POKE学习笔记
笔记·学习
li星野2 小时前
DeepSeek提示词使用
人工智能·学习·deepseek
努力学习的明2 小时前
JVM 学习路线与实战指南:内存管理、GC 机制及问题诊断
jvm·学习
狮驼岭的小钻风2 小时前
python系统学习
学习
一定要AK9 小时前
刷题时的学习笔记
c++·笔记·学习
xxxibolva10 小时前
SQL 学习
数据库·sql·学习
星辰即远方12 小时前
OC学习Foudation框架
学习·ios·objective-c
yyk的萌13 小时前
AI 应用开发工程师基础学习计划
开发语言·python·学习·ai·lua
龘龍龙15 小时前
大模型学习(三)-RAG、LangChain
学习·langchain