一、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 唯一索引
📚 基本概念
- 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 基础虚拟字段
- 基础 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);
- 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()); // 默认不包含虚拟字段
- 在查询结果中包含虚拟字段
方法一:使用 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"
}
}
])