Node.js-MongoDB数据库

Node.js MongoDB 数据库

1. MongoDB 简介

MongoDB 是一个基于文档的 NoSQL 数据库,使用 JSON 格式存储数据,与 Node.js 配合使用非常方便。

1.1 特点

  • 文档型数据库: 使用 BSON (Binary JSON) 格式存储
  • 无模式 (Schema-less): 灵活的数据结构
  • 高性能: 支持索引和查询优化
  • 可扩展: 支持水平扩展(分片)
  • 丰富的查询: 支持复杂查询、聚合等

1.2 核心概念

MongoDB 关系型数据库 说明
Database Database 数据库
Collection Table 集合/表
Document Row 文档/行
Field Column 字段/列
Index Index 索引

1.3 安装 MongoDB

# Windows
# 从官网下载安装: https://www.mongodb.***/try/download/***munity

# macOS (使用 Homebrew)
brew tap mongodb/brew
brew install mongodb-***munity

# Linux (Ubuntu)
sudo apt-get install mongodb

# 启动 MongoDB
mongod

# 连接 MongoDB Shell
mongo

2. 使用 MongoDB 驱动

2.1 安装

# 安装官方 MongoDB 驱动
npm install mongodb

# 或使用 Mongoose (推荐)
npm install mongoose

2.2 连接数据库 (原生驱动)

const { MongoClient } = require('mongodb');  // 导入MongoDB客户端

// 连接 URL (格式: mongodb://主机:端口)
const url = 'mongodb://localhost:27017';  // 本地MongoDB服务器,默认端口27017
const dbName = 'myapp';  // 要连接的数据库名称

// 创建客户端实例
const client = new MongoClient(url);

async function main() {
  try {
    // 连接到服务器(异步操作,返回Promise)
    await client.connect();
    console.log('成功连接到 MongoDB');  // 输出: 成功连接到 MongoDB

    const db = client.db(dbName);  // 获取数据库实例
    const collection = db.collection('users');  // 获取集合(类似关系型数据库的表)

    // 执行操作...
    // 在这里可以执行插入、查询、更新、删除等操作

  } catch (err) {
    console.error('连接错误:', err);  // 捕获并输出连接错误
  } finally {
    // 关闭连接(释放资源,避免内存泄漏)
    await client.close();
  }
}

main();  // 执行主函数

⚠️ 注意事项:

  • 连接池: MongoClient会自动创建连接池,不需要每次操作都创建新客户端
  • 关闭连接: 应在应用退出时关闭连接,不要每次操作后都关闭
  • 错误处理: 必须使用try-catch捕获异步操作的错误
  • URL格式: 完整格式为 mongodb://[username:password@]host:port/[database][?options]
  • 数据库自动创建: 如果指定的数据库不存在,MongoDB会在首次插入数据时自动创建
  • 集合自动创建: 如果集合不存在,在首次插入文档时会自动创建
// 常见错误:每次操作都创建新连接
async function badExample() {
  const client = new MongoClient(url);
  await client.connect();
  await client.db('test').collection('users').findOne();
  await client.close();  // ❌ 频繁连接和关闭,性能差
}

// 正确做法:复用连接
const client = new MongoClient(url);
await client.connect();  // 应用启动时连接一次

async function goodExample() {
  const users = await client.db('test').collection('users').find().toArray();
  // ✅ 复用已建立的连接
}

// 应用退出时关闭
process.on('SIGINT', async () => {
  await client.close();
  process.exit(0);
});

// 带认证的连接URL
const authUrl = 'mongodb://admin:password@localhost:27017/myapp?authSource=admin';

// 连接MongoDB Atlas(云服务)
const atlasUrl = 'mongodb+srv://username:password@cluster0.mongodb.***/myapp?retryWrites=true&w=majority';

🎯 实际应用场景:

// 场景1:Express应用中复用连接
// db.js
const { MongoClient } = require('mongodb');
const url = 'mongodb://localhost:27017';
let db = null;

async function connectDB() {
  if (db) return db;  // 已连接则返回现有实例

  const client = new MongoClient(url);
  await client.connect();
  db = client.db('myapp');
  console.log('数据库已连接');
  return db;
}

module.exports = { connectDB };

// app.js
const express = require('express');
const { connectDB } = require('./db');
const app = express();

app.get('/users', async (req, res) => {
  const db = await connectDB();
  const users = await db.collection('users').find().toArray();
  res.json(users);
});

// 场景2:环境配置管理
const config = {
  development: {
    url: 'mongodb://localhost:27017',
    dbName: 'myapp_dev'
  },
  production: {
    url: process.env.MONGODB_URI,
    dbName: 'myapp_prod'
  }
};

const env = process.env.NODE_ENV || 'development';
const { url, dbName } = config[env];

// 场景3:连接池配置
const client = new MongoClient(url, {
  maxPoolSize: 50,           // 最大连接数
  minPoolSize: 10,           // 最小连接数
  maxIdleTimeMS: 30000,      // 连接最大空闲时间
  serverSelectionTimeoutMS: 5000,  // 服务器选择超时
  socketTimeoutMS: 45000     // Socket超时时间
});

// 场景4:健康检查
async function checkDBHealth() {
  try {
    await client.db('admin').***mand({ ping: 1 });
    console.log('数据库连接正常');
    return true;
  } catch (err) {
    console.error('数据库连接异常:', err);
    return false;
  }
}

// 场景5:优雅关闭
const gracefulShutdown = async (signal) => {
  console.log(`\n收到${signal}信号,正在关闭数据库连接...`);
  await client.close();
  console.log('数据库连接已关闭');
  process.exit(0);
};

process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));

2.3 CRUD 操作 (原生驱动)

const { MongoClient, ObjectId } = require('mongodb');  // ObjectId用于操作MongoDB的_id字段

const url = 'mongodb://localhost:27017';
const client = new MongoClient(url);

async function crudOperations() {
  try {
    await client.connect();
    const db = client.db('myapp');
    const users = db.collection('users');  // 获取users集合的引用

    // ========== CREATE 创建 ==========

    // 插入单个文档
    const insertResult = await users.insertOne({
      name: 'Alice',
      email: 'alice@example.***',
      age: 25,
      createdAt: new Date()  // 自动添加创建时间
    });
    console.log('插入的文档 ID:', insertResult.insertedId);
    // 输出: 插入的文档 ID: 507f1f77bcf86cd799439011 (自动生成的ObjectId)

    // 插入多个文档
    const insertManyResult = await users.insertMany([
      { name: 'Bob', email: 'bob@example.***', age: 30 },
      { name: 'Charlie', email: 'charlie@example.***', age: 35 }
    ]);
    console.log('插入的文档数:', insertManyResult.insertedCount);
    // 输出: 插入的文档数: 2

    // ========== READ 读取 ==========

    // 查找所有文档
    const allUsers = await users.find({}).toArray();  // find({})表示无条件查询,toArray()将游标转为数组
    console.log('所有用户:', allUsers);
    // 输出: 所有用户: [{_id: ..., name: 'Alice', ...}, {_id: ..., name: 'Bob', ...}]

    // 查找单个文档
    const user = await users.findOne({ name: 'Alice' });  // findOne返回第一个匹配的文档
    console.log('找到的用户:', user);
    // 输出: 找到的用户: {_id: ..., name: 'Alice', email: 'alice@example.***', age: 25}

    // 条件查询
    const adults = await users.find({ age: { $gte: 18 } }).toArray();  // $gte表示大于等于
    console.log('成年用户:', adults);
    // 输出: 成年用户: [{name: 'Alice', age: 25}, {name: 'Bob', age: 30}, ...]

    // 查询并排序
    const sortedUsers = await users
      .find({})
      .sort({ age: -1 })  // -1 降序(从大到小), 1 升序(从小到大)
      .toArray();
    // 输出结果按age降序排列

    // 查询并限制数量
    const limitedUsers = await users
      .find({})
      .limit(10)    // 最多返回10条
      .skip(0)      // 跳过前0个(用于分页)
      .toArray();
    // 分页示例: skip(20).limit(10) 表示跳过前20条,取第21-30条

    // 查询特定字段(投影)
    const userNames = await users
      .find({}, { projection: { name: 1, email: 1, _id: 0 } })  // 1表示包含,0表示排除
      .toArray();
    // 输出: [{name: 'Alice', email: 'alice@example.***'}, ...] (不包含_id字段)

    // ========== UPDATE 更新 ==========

    // 更新单个文档
    const updateResult = await users.updateOne(
      { name: 'Alice' },           // 查询条件
      { $set: { age: 26, updatedAt: new Date() } }  // $set操作符用于设置字段值
    );
    console.log('修改的文档数:', updateResult.modifiedCount);
    // 输出: 修改的文档数: 1

    // 更新多个文档
    const updateManyResult = await users.updateMany(
      { age: { $lt: 30 } },        // 年龄小于30的所有文档
      { $set: { category: 'young' } }  // 添加category字段
    );
    console.log('修改的文档数:', updateManyResult.modifiedCount);
    // 输出: 修改的文档数: 2 (假设有2个文档age<30)

    // 替换文档
    const replaceResult = await users.replaceOne(
      { name: 'Bob' },  // 查询条件
      { name: 'Bob Smith', email: 'bob.smith@example.***', age: 31 }  // 完全替换(不保留原有字段)
    );
    // ⚠️ replaceOne会替换整个文档,除了_id字段

    // ========== DELETE 删除 ==========

    // 删除单个文档
    const deleteResult = await users.deleteOne({ name: 'Charlie' });  // 删除第一个匹配的文档
    console.log('删除的文档数:', deleteResult.deletedCount);
    // 输出: 删除的文档数: 1

    // 删除多个文档
    const deleteManyResult = await users.deleteMany({ age: { $lt: 25 } });  // 删除所有age<25的文档
    console.log('删除的文档数:', deleteManyResult.deletedCount);
    // 输出: 删除的文档数: 0 (如果没有age<25的文档)

  } finally {
    await client.close();
  }
}

crudOperations();

⚠️ 注意事项:

  • 异步操作: 所有MongoDB操作都返回Promise,必须使用await或.then()
  • 查询游标: find()返回游标,需要.toArray()转换为数组
  • _id字段: MongoDB自动为每个文档生成唯一的_id字段(ObjectId类型)
  • 更新操作符: 必须使用set、set、setinc等操作符,直接传对象会报错
  • 投影字段: projection中1表示包含,0表示排除,不能混用(除了_id可以单独排除)
  • 删除操作: deleteOne/deleteMany是永久删除,无法恢复,慎用
  • 性能考虑: 大量数据查询时使用skip()会影响性能,建议使用基于_id的分页
// 常见错误:更新时忘记使用$set
await users.updateOne(
  { name: 'Alice' },
  { age: 26 }  // ❌ 错误:会替换整个文档
);

await users.updateOne(
  { name: 'Alice' },
  { $set: { age: 26 } }  // ✅ 正确:使用$set操作符
);

// 常见错误:投影字段混用包含和排除
const users = await collection.find({}, {
  projection: { name: 1, age: 0 }  // ❌ 错误:不能混用1和0
}).toArray();

const users = await collection.find({}, {
  projection: { name: 1, email: 1, _id: 0 }  // ✅ 正确:_id可以单独排除
}).toArray();

// 常见错误:忘记toArray()
const users = await collection.find({});  // ❌ 返回游标对象,不是数组
console.log(users);  // Cursor {...}

const users = await collection.find({}).toArray();  // ✅ 转换为数组
console.log(users);  // [{...}, {...}, ...]

// 性能问题:大数据量分页使用skip()
const page = 1000;
const pageSize = 10;
const users = await collection
  .find({})
  .skip(page * pageSize)  // ❌ 跳过10000条,性能差
  .limit(pageSize)
  .toArray();

// ✅ 基于_id的分页(更高效)
const lastId = '507f1f77bcf86cd799439011';  // 上一页最后一个文档的_id
const users = await collection
  .find({ _id: { $gt: new ObjectId(lastId) } })
  .limit(pageSize)
  .toArray();

🎯 实际应用场景:

// 场景1:用户注册功能
async function registerUser(userData) {
  const db = await connectDB();
  const users = db.collection('users');

  // 检查邮箱是否已存在
  const existing = await users.findOne({ email: userData.email });
  if (existing) {
    throw new Error('邮箱已被注册');
  }

  // 插入新用户
  const result = await users.insertOne({
    ...userData,
    password: await hashPassword(userData.password),  // 密码加密
    createdAt: new Date(),
    status: 'active'
  });

  return result.insertedId;
}

// 场景2:博客文章列表分页
async function getArticles(page = 1, pageSize = 10) {
  const db = await connectDB();
  const articles = db.collection('articles');

  // 计算总数
  const total = await articles.countDocuments({ published: true });

  // 分页查询
  const list = await articles
    .find({ published: true })
    .sort({ createdAt: -1 })  // 最新文章在前
    .skip((page - 1) * pageSize)
    .limit(pageSize)
    .project({ title: 1, summary: 1, author: 1, createdAt: 1 })  // 只返回需要的字段
    .toArray();

  return {
    list,
    total,
    page,
    pageSize,
    totalPages: Math.ceil(total / pageSize)
  };
}

// 场景3:文章浏览量增加
async function incrementViews(articleId) {
  const db = await connectDB();
  const articles = db.collection('articles');

  // 使用$inc操作符原子性地增加浏览量
  const result = await articles.updateOne(
    { _id: new ObjectId(articleId) },
    {
      $inc: { views: 1 },  // 浏览量+1
      $set: { lastViewedAt: new Date() }  // 记录最后浏览时间
    }
  );

  return result.modifiedCount > 0;
}

// 场景4:批量更新文章状态
async function publishArticles(articleIds) {
  const db = await connectDB();
  const articles = db.collection('articles');

  // 批量更新多个文档
  const result = await articles.updateMany(
    { _id: { $in: articleIds.map(id => new ObjectId(id)) } },  // 匹配多个_id
    {
      $set: {
        published: true,
        publishedAt: new Date()
      }
    }
  );

  return result.modifiedCount;
}

// 场景5:搜索功能(多条件查询)
async function searchProducts(filters) {
  const db = await connectDB();
  const products = db.collection('products');

  // 构建查询条件
  const query = {};

  if (filters.keyword) {
    query.$or = [  // 多字段模糊搜索
      { name: { $regex: filters.keyword, $options: 'i' } },  // 不区分大小写
      { description: { $regex: filters.keyword, $options: 'i' } }
    ];
  }

  if (filters.category) {
    query.category = filters.category;
  }

  if (filters.minPrice || filters.maxPrice) {
    query.price = {};
    if (filters.minPrice) query.price.$gte = filters.minPrice;
    if (filters.maxPrice) query.price.$lte = filters.maxPrice;
  }

  if (filters.inStock) {
    query.stock = { $gt: 0 };
  }

  // 执行查询
  const products = await products
    .find(query)
    .sort({ [filters.sortBy || 'createdAt']: filters.sortOrder === 'asc' ? 1 : -1 })
    .limit(filters.limit || 20)
    .toArray();

  return products;
}

3. Mongoose ODM

Mongoose 是 MongoDB 的对象数据模型 (ODM),提供了模式定义、验证、类型转换等功能。

3.1 连接数据库

const mongoose = require('mongoose');

// 连接数据库
mongoose.connect('mongodb://localhost:27017/myapp', {
  useNewUrlParser: true,
  useUnifiedTopology: true
});

// 监听连接事件
const db = mongoose.connection;

db.on('error', console.error.bind(console, '连接错误:'));
db.once('open', () => {
  console.log('已连接到 MongoDB');
});

// 优雅关闭
process.on('SIGINT', async () => {
  await mongoose.connection.close();
  console.log('MongoDB 连接已关闭');
  process.exit(0);
});

3.2 定义模式 (Schema)

const mongoose = require('mongoose');

// 定义用户模式
const userSchema = new mongoose.Schema({
  // 基本类型
  name: {
    type: String,
    required: [true, '姓名是必需的'],
    trim: true,
    minlength: [2, '姓名至少2个字符'],
    maxlength: [50, '姓名最多50个字符']
  },

  email: {
    type: String,
    required: true,
    unique: true,
    lowercase: true,
    match: [/^\S+@\S+\.\S+$/, '请输入有效的邮箱']
  },

  age: {
    type: Number,
    min: [0, '年龄不能为负数'],
    max: [150, '年龄不能超过150']
  },

  // 枚举
  role: {
    type: String,
    enum: ['user', 'admin', 'moderator'],
    default: 'user'
  },

  // 布尔值
  isActive: {
    type: Boolean,
    default: true
  },

  // 数组
  tags: [String],

  hobbies: [{
    type: String,
    trim: true
  }],

  // 嵌套对象
  address: {
    street: String,
    city: String,
    zipCode: String,
    country: {
      type: String,
      default: 'USA'
    }
  },

  // 引用其他文档
  posts: [{
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Post'
  }],

  // 日期
  createdAt: {
    type: Date,
    default: Date.now
  },

  updatedAt: Date

}, {
  // Schema 选项
  timestamps: true, // 自动添加 createdAt 和 updatedAt
  collection: 'users' // 指定集合名称
});

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

module.exports = User;

3.3 模式类型

const schema = new mongoose.Schema({
  // 字符串
  name: String,

  // 数字
  age: Number,

  // 布尔值
  isActive: Boolean,

  // 日期
  birthday: Date,

  // Buffer
  data: Buffer,

  // ObjectId
  userId: mongoose.Schema.Types.ObjectId,

  // Mixed (任意类型)
  metadata: mongoose.Schema.Types.Mixed,

  // 数组
  tags: [String],
  numbers: [Number],

  // Decimal128 (高精度数字)
  price: mongoose.Schema.Types.Decimal128,

  // Map
  socialProfiles: {
    type: Map,
    of: String
  }
});

3.4 CRUD 操作 (Mongoose)

const mongoose = require('mongoose');
const User = require('./models/User');

// 连接数据库
mongoose.connect('mongodb://localhost:27017/myapp');

async function mongooseOperations() {
  try {
    // ========== CREATE 创建 ==========

    // 方式 1: 使用 new 和 save()
    const user1 = new User({
      name: 'Alice',
      email: 'alice@example.***',
      age: 25,
      role: 'user',
      tags: ['javascript', 'mongodb']
    });
    await user1.save();
    console.log('用户已保存:', user1);

    // 方式 2: 使用 create()
    const user2 = await User.create({
      name: 'Bob',
      email: 'bob@example.***',
      age: 30
    });
    console.log('用户已创建:', user2);

    // 批量创建
    const users = await User.insertMany([
      { name: 'Charlie', email: 'charlie@example.***', age: 35 },
      { name: 'David', email: 'david@example.***', age: 28 }
    ]);
    console.log('批量创建:', users.length, '个用户');

    // ========== READ 读取 ==========

    // 查找所有
    const allUsers = await User.find();
    console.log('所有用户:', allUsers);

    // 条件查询
    const activeUsers = await User.find({ isActive: true });

    // 查询单个
    const user = await User.findOne({ email: 'alice@example.***' });
    console.log('找到的用户:', user);

    // 通过 ID 查询
    const userById = await User.findById('507f1f77bcf86cd799439011');

    // 链式查询
    const results = await User
      .find({ age: { $gte: 18 } })
      .where('isActive').equals(true)
      .select('name email age') // 选择字段
      .sort({ age: -1 })        // 排序
      .limit(10)                // 限制数量
      .skip(0)                  // 跳过
      .exec();

    // 计数
    const count = await User.countDocuments({ role: 'admin' });

    // 是否存在
    const exists = await User.exists({ email: 'test@example.***' });

    // ========== UPDATE 更新 ==========

    // 查找并更新
    const updated1 = await User.findOneAndUpdate(
      { email: 'alice@example.***' },
      { $set: { age: 26 } },
      { new: true } // 返回更新后的文档
    );

    // 通过 ID 更新
    const updated2 = await User.findByIdAndUpdate(
      user1._id,
      { $set: { age: 27 } },
      { new: true, runValidators: true } // 运行验证器
    );

    // 更新多个
    const updateResult = await User.updateMany(
      { age: { $lt: 30 } },
      { $set: { category: 'young' } }
    );
    console.log('修改了', updateResult.modifiedCount, '个文档');

    // 使用 save() 更新
    user1.age = 28;
    user1.tags.push('express');
    await user1.save();

    // ========== DELETE 删除 ==========

    // 查找并删除
    const deleted1 = await User.findOneAndDelete({ name: 'Charlie' });

    // 通过 ID 删除
    const deleted2 = await User.findByIdAndDelete(user2._id);

    // 删除多个
    const deleteResult = await User.deleteMany({ isActive: false });
    console.log('删除了', deleteResult.deletedCount, '个文档');

  } catch (err) {
    console.error('操作错误:', err);
  } finally {
    await mongoose.connection.close();
  }
}

mongooseOperations();

3.5 查询操作符

// 比较操作符
User.find({ age: { $eq: 25 } });      // 等于
User.find({ age: { $ne: 25 } });      // 不等于
User.find({ age: { $gt: 25 } });      // 大于
User.find({ age: { $gte: 25 } });     // 大于等于
User.find({ age: { $lt: 30 } });      // 小于
User.find({ age: { $lte: 30 } });     // 小于等于
User.find({ age: { $in: [20, 25, 30] } }); // 在数组中
User.find({ age: { $nin: [20, 25] } }); // 不在数组中

// 逻辑操作符
User.find({ $and: [{ age: { $gte: 18 } }, { isActive: true }] });
User.find({ $or: [{ role: 'admin' }, { role: 'moderator' }] });
User.find({ $nor: [{ age: { $lt: 18 } }, { isActive: false }] });
User.find({ age: { $not: { $lt: 18 } } });

// 元素操作符
User.find({ address: { $exists: true } }); // 字段存在
User.find({ age: { $type: 'number' } });   // 类型检查

// 数组操作符
User.find({ tags: { $all: ['javascript', 'mongodb'] } }); // 包含所有
User.find({ tags: 'javascript' });                        // 包含元素
User.find({ tags: { $size: 3 } });                        // 数组大小

// 正则表达式
User.find({ name: /^A/i });                // 以 A 开头(不区分大小写)
User.find({ email: { $regex: '@gmail.***$' } }); // 邮箱结尾

// 文本搜索(需要创建文本索引)
User.find({ $text: { $search: 'alice bob' } });

3.6 实例方法和静态方法

const userSchema = new mongoose.Schema({
  name: String,
  email: String,
  age: Number
});

// ========== 实例方法 ==========
// 在文档实例上调用

userSchema.methods.getFullInfo = function() {
  return `${this.name} (${this.email})`;
};

userSchema.methods.isAdult = function() {
  return this.age >= 18;
};

// ========== 静态方法 ==========
// 在模型上调用

userSchema.statics.findByEmail = function(email) {
  return this.findOne({ email });
};

userSchema.statics.findAdults = function() {
  return this.find({ age: { $gte: 18 } });
};

const User = mongoose.model('User', userSchema);

// 使用
async function example() {
  const user = await User.findOne({ name: 'Alice' });

  // 调用实例方法
  console.log(user.getFullInfo());
  console.log('是成年人?', user.isAdult());

  // 调用静态方法
  const userByEmail = await User.findByEmail('alice@example.***');
  const adults = await User.findAdults();
}

3.7 虚拟属性

const userSchema = new mongoose.Schema({
  firstName: String,
  lastName: String,
  email: String
});

// 定义虚拟属性(不存储在数据库中)
userSchema.virtual('fullName').get(function() {
  return `${this.firstName} ${this.lastName}`;
});

userSchema.virtual('fullName').set(function(name) {
  const parts = name.split(' ');
  this.firstName = parts[0];
  this.lastName = parts[1];
});

// 在 JSON 输出中包含虚拟属性
userSchema.set('toJSON', { virtuals: true });
userSchema.set('toObject', { virtuals: true });

const User = mongoose.model('User', userSchema);

// 使用
const user = new User({
  firstName: 'John',
  lastName: 'Doe'
});

console.log(user.fullName); // "John Doe"

user.fullName = 'Jane Smith';
console.log(user.firstName); // "Jane"
console.log(user.lastName);  // "Smith"

3.8 中间件 (Hooks)

const userSchema = new mongoose.Schema({
  email: String,
  password: String,
  createdAt: Date,
  updatedAt: Date
});

// ========== Pre Hook (前置钩子) ==========

// 保存前执行
userSchema.pre('save', function(next) {
  console.log('准备保存文档...');

  // 更新时间戳
  this.updatedAt = Date.now();

  if (this.isNew) {
    this.createdAt = Date.now();
  }

  next();
});

// 异步钩子
userSchema.pre('save', async function() {
  if (this.isModified('password')) {
    const bcrypt = require('bcrypt');
    this.password = await bcrypt.hash(this.password, 10);
  }
});

// 查询前执行
userSchema.pre('find', function() {
  console.log('准备查询...');
  this.where({ isActive: true }); // 自动添加条件
});

// ========== Post Hook (后置钩子) ==========

// 保存后执行
userSchema.post('save', function(doc) {
  console.log('文档已保存:', doc._id);
});

// 删除后执行
userSchema.post('remove', function(doc) {
  console.log('文档已删除:', doc._id);
  // 可以在这里清理相关数据
});

// 错误处理钩子
userSchema.post('save', function(error, doc, next) {
  if (error.name === 'MongoServerError' && error.code === 11000) {
    next(new Error('邮箱已存在'));
  } else {
    next(error);
  }
});

const User = mongoose.model('User', userSchema);

4. 关系和引用

4.1 嵌入式文档

// 适用于一对少量的关系
const userSchema = new mongoose.Schema({
  name: String,
  email: String,
  addresses: [{
    street: String,
    city: String,
    zipCode: String,
    isPrimary: {
      type: Boolean,
      default: false
    }
  }]
});

const User = mongoose.model('User', userSchema);

// 使用
const user = new User({
  name: 'Alice',
  email: 'alice@example.***',
  addresses: [
    { street: '123 Main St', city: 'Boston', zipCode: '02101', isPrimary: true },
    { street: '456 Oak Ave', city: 'Cambridge', zipCode: '02139' }
  ]
});

await user.save();

// 添加地址
user.addresses.push({ street: '789 Elm St', city: 'Somerville', zipCode: '02144' });
await user.save();

4.2 引用 (Population)

// User 模型
const userSchema = new mongoose.Schema({
  name: String,
  email: String
});

const User = mongoose.model('User', userSchema);

// Post 模型
const postSchema = new mongoose.Schema({
  title: String,
  content: String,
  author: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User', // 引用 User 模型
    required: true
  },
  ***ments: [{
    user: {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'User'
    },
    text: String,
    createdAt: {
      type: Date,
      default: Date.now
    }
  }],
  tags: [String],
  createdAt: {
    type: Date,
    default: Date.now
  }
});

const Post = mongoose.model('Post', postSchema);

// 创建数据
async function createData() {
  const user = await User.create({
    name: 'Alice',
    email: 'alice@example.***'
  });

  const post = await Post.create({
    title: '我的第一篇博客',
    content: '这是内容...',
    author: user._id
  });
}

// 填充引用(Population)
async function getPosts() {
  // 基本填充
  const posts = await Post.find().populate('author');
  console.log(posts[0].author.name); // 'Alice'

  // 选择字段
  const posts2 = await Post.find()
    .populate('author', 'name email -_id');

  // 多层填充
  const posts3 = await Post.find()
    .populate({
      path: 'author',
      select: 'name email'
    })
    .populate({
      path: '***ments.user',
      select: 'name'
    });

  // 条件填充
  const posts4 = await Post.find()
    .populate({
      path: 'author',
      match: { isActive: true },
      select: 'name email'
    });
}

5. 索引

5.1 创建索引

const userSchema = new mongoose.Schema({
  email: {
    type: String,
    unique: true,      // 唯一索引
    index: true        // 普通索引
  },

  username: {
    type: String,
    unique: true,
    sparse: true       // 稀疏索引(允许null)
  },

  firstName: String,
  lastName: String,
  location: {
    type: {
      type: String,
      enum: ['Point'],
      default: 'Point'
    },
    coordinates: {
      type: [Number],
      index: '2dsphere'  // 地理空间索引
    }
  }
});

// 复合索引
userSchema.index({ firstName: 1, lastName: 1 });

// 文本索引
userSchema.index({ firstName: 'text', lastName: 'text' });

// TTL 索引(自动删除过期文档)
userSchema.index({ createdAt: 1 }, { expireAfterSeconds: 3600 });

const User = mongoose.model('User', userSchema);

// 手动创建索引
User.createIndexes();

5.2 查看和管理索引

// 获取所有索引
const indexes = await User.collection.getIndexes();
console.log(indexes);

// 删除索引
await User.collection.dropIndex('email_1');

// 删除所有索引(除了 _id)
await User.collection.dropIndexes();

// 重建索引
await User.syncIndexes();

6. 聚合 (Aggregation)

const Order = mongoose.model('Order', new mongoose.Schema({
  product: String,
  quantity: Number,
  price: Number,
  customer: String,
  createdAt: { type: Date, default: Date.now }
}));

async function aggregationExamples() {
  // 基本聚合
  const results = await Order.aggregate([
    // $match - 过滤文档
    { $match: { quantity: { $gte: 10 } } },

    // $group - 分组
    {
      $group: {
        _id: '$product',
        totalQuantity: { $sum: '$quantity' },
        totalRevenue: { $sum: { $multiply: ['$quantity', '$price'] } },
        avgPrice: { $avg: '$price' },
        count: { $sum: 1 }
      }
    },

    // $sort - 排序
    { $sort: { totalRevenue: -1 } },

    // $limit - 限制结果
    { $limit: 10 },

    // $project - 选择/重命名字段
    {
      $project: {
        product: '$_id',
        totalQuantity: 1,
        totalRevenue: 1,
        avgPrice: { $round: ['$avgPrice', 2] },
        _id: 0
      }
    }
  ]);

  console.log(results);

  // 更复杂的聚合
  const salesByMonth = await Order.aggregate([
    {
      $group: {
        _id: {
          year: { $year: '$createdAt' },
          month: { $month: '$createdAt' }
        },
        totalSales: { $sum: { $multiply: ['$quantity', '$price'] } },
        orderCount: { $sum: 1 }
      }
    },
    {
      $sort: { '_id.year': -1, '_id.month': -1 }
    },
    {
      $project: {
        date: {
          $concat: [
            { $toString: '$_id.year' },
            '-',
            { $toString: '$_id.month' }
          ]
        },
        totalSales: 1,
        orderCount: 1,
        avgOrderValue: { $divide: ['$totalSales', '$orderCount'] }
      }
    }
  ]);

  // $lookup - 关联查询(类似 SQL JOIN)
  const ordersWithCustomers = await Order.aggregate([
    {
      $lookup: {
        from: 'customers',
        localField: 'customer',
        foreignField: '_id',
        as: 'customerInfo'
      }
    },
    {
      $unwind: '$customerInfo' // 展开数组
    }
  ]);
}

7. 实战案例

案例 1: 完整的用户系统

const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');

// 用户模式
const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
    unique: true,
    trim: true,
    minlength: 3,
    maxlength: 30
  },

  email: {
    type: String,
    required: true,
    unique: true,
    lowercase: true,
    match: /^\S+@\S+\.\S+$/
  },

  password: {
    type: String,
    required: true,
    minlength: 6
  },

  profile: {
    firstName: String,
    lastName: String,
    avatar: String,
    bio: String,
    dateOfBirth: Date
  },

  role: {
    type: String,
    enum: ['user', 'admin', 'moderator'],
    default: 'user'
  },

  isActive: {
    type: Boolean,
    default: true
  },

  lastLogin: Date,

  refreshTokens: [String]

}, {
  timestamps: true
});

// 密码加密(保存前)
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();

  try {
    const salt = await bcrypt.genSalt(10);
    this.password = await bcrypt.hash(this.password, salt);
    next();
  } catch (err) {
    next(err);
  }
});

// 验证密码
userSchema.methods.***parePassword = async function(candidatePassword) {
  return await bcrypt.***pare(candidatePassword, this.password);
};

// 生成访问令牌
userSchema.methods.generateA***essToken = function() {
  return jwt.sign(
    { id: this._id, role: this.role },
    process.env.A***ESS_TOKEN_SECRET,
    { expiresIn: '15m' }
  );
};

// 生成刷新令牌
userSchema.methods.generateRefreshToken = function() {
  return jwt.sign(
    { id: this._id },
    process.env.REFRESH_TOKEN_SECRET,
    { expiresIn: '7d' }
  );
};

// 获取公开信息
userSchema.methods.toPublicJSON = function() {
  return {
    id: this._id,
    username: this.username,
    email: this.email,
    profile: this.profile,
    role: this.role,
    createdAt: this.createdAt
  };
};

// 静态方法:通过邮箱查找
userSchema.statics.findByEmail = function(email) {
  return this.findOne({ email: email.toLowerCase() });
};

const User = mongoose.model('User', userSchema);

module.exports = User;
// 使用用户模型
const User = require('./models/User');

// 注册用户
async function registerUser(userData) {
  try {
    const user = new User(userData);
    await user.save();

    return {
      su***ess: true,
      user: user.toPublicJSON()
    };
  } catch (err) {
    if (err.code === 11000) {
      return { su***ess: false, error: '用户名或邮箱已存在' };
    }
    return { su***ess: false, error: err.message };
  }
}

// 登录
async function loginUser(email, password) {
  try {
    const user = await User.findByEmail(email);

    if (!user) {
      return { su***ess: false, error: '用户不存在' };
    }

    const isMatch = await user.***parePassword(password);

    if (!isMatch) {
      return { su***ess: false, error: '密码错误' };
    }

    // 更新最后登录时间
    user.lastLogin = new Date();
    await user.save();

    // 生成令牌
    const a***essToken = user.generateA***essToken();
    const refreshToken = user.generateRefreshToken();

    // 保存刷新令牌
    user.refreshTokens.push(refreshToken);
    await user.save();

    return {
      su***ess: true,
      a***essToken,
      refreshToken,
      user: user.toPublicJSON()
    };
  } catch (err) {
    return { su***ess: false, error: err.message };
  }
}

// 使用示例
async function example() {
  // 注册
  const result = await registerUser({
    username: 'alice',
    email: 'alice@example.***',
    password: 'password123',
    profile: {
      firstName: 'Alice',
      lastName: 'Smith'
    }
  });

  console.log(result);

  // 登录
  const loginResult = await loginUser('alice@example.***', 'password123');
  console.log(loginResult);
}

案例 2: 博客系统

// models/Post.js
const mongoose = require('mongoose');

const ***mentSchema = new mongoose.Schema({
  author: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },
  content: {
    type: String,
    required: true,
    maxlength: 500
  },
  likes: [{
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  }],
  createdAt: {
    type: Date,
    default: Date.now
  }
});

const postSchema = new mongoose.Schema({
  title: {
    type: String,
    required: true,
    trim: true,
    minlength: 5,
    maxlength: 100
  },

  slug: {
    type: String,
    unique: true,
    lowercase: true
  },

  content: {
    type: String,
    required: true
  },

  author: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },

  tags: [{
    type: String,
    trim: true,
    lowercase: true
  }],

  category: {
    type: String,
    enum: ['tech', 'lifestyle', 'travel', 'food', 'other'],
    default: 'other'
  },

  coverImage: String,

  published: {
    type: Boolean,
    default: false
  },

  publishedAt: Date,

  views: {
    type: Number,
    default: 0
  },

  likes: [{
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  }],

  ***ments: [***mentSchema],

  featured: {
    type: Boolean,
    default: false
  }

}, {
  timestamps: true
});

// 索引
postSchema.index({ title: 'text', content: 'text' });
postSchema.index({ slug: 1 });
postSchema.index({ tags: 1 });
postSchema.index({ createdAt: -1 });

// 生成 slug
postSchema.pre('validate', function(next) {
  if (this.title && !this.slug) {
    this.slug = this.title
      .toLowerCase()
      .replace(/[^\w\s-]/g, '')
      .replace(/\s+/g, '-')
      .replace(/-+/g, '-')
      .trim();
  }
  next();
});

// 发布博客
postSchema.methods.publish = function() {
  this.published = true;
  this.publishedAt = new Date();
  return this.save();
};

// 增加浏览量
postSchema.methods.incrementViews = function() {
  this.views++;
  return this.save();
};

// 切换点赞
postSchema.methods.toggleLike = function(userId) {
  const index = this.likes.indexOf(userId);

  if (index === -1) {
    this.likes.push(userId);
  } else {
    this.likes.splice(index, 1);
  }

  return this.save();
};

// 添加评论
postSchema.methods.add***ment = function(userId, content) {
  this.***ments.push({
    author: userId,
    content
  });
  return this.save();
};

// 静态方法:获取热门文章
postSchema.statics.getPopular = function(limit = 10) {
  return this.find({ published: true })
    .sort({ views: -1, likes: -1 })
    .limit(limit)
    .populate('author', 'username profile.avatar');
};

// 静态方法:搜索文章
postSchema.statics.search = function(query) {
  return this.find({
    $text: { $search: query },
    published: true
  }).select({ score: { $meta: 'textScore' } })
    .sort({ score: { $meta: 'textScore' } });
};

const Post = mongoose.model('Post', postSchema);

module.exports = Post;

8. 性能优化

8.1 使用精简查询

// ❌ 不好:查询所有字段
const users = await User.find();

// ✅ 好:只查询需要的字段
const users = await User.find().select('name email -_id');

// ✅ 使用 lean() 返回普通 JavaScript 对象
const users = await User.find().lean();

8.2 批量操作

// ❌ 不好:循环插入
for (const user of users) {
  await User.create(user);
}

// ✅ 好:批量插入
await User.insertMany(users);

// 批量更新
await User.bulkWrite([
  {
    updateOne: {
      filter: { _id: id1 },
      update: { $set: { status: 'active' } }
    }
  },
  {
    deleteOne: {
      filter: { _id: id2 }
    }
  }
]);

8.3 连接池配置

mongoose.connect('mongodb://localhost:27017/myapp', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  poolSize: 10,          // 连接池大小
  socketTimeoutMS: 45000,
  family: 4              // 使用 IPv4
});

9. 最佳实践

9.1 错误处理

// 全局错误处理
mongoose.connection.on('error', (err) => {
  console.error('MongoDB 连接错误:', err);
});

// 查询错误处理
try {
  const user = await User.findById(id);
  if (!user) {
    throw new Error('用户不存在');
  }
} catch (err) {
  if (err.name === 'CastError') {
    console.error('无效的 ID 格式');
  } else {
    console.error('查询错误:', err);
  }
}

9.2 数据验证

const userSchema = new mongoose.Schema({
  email: {
    type: String,
    required: [true, '邮箱是必需的'],
    validate: {
      validator: function(v) {
        return /^\S+@\S+\.\S+$/.test(v);
      },
      message: props => `${props.value} 不是有效的邮箱地址`
    }
  },

  age: {
    type: Number,
    min: [0, '年龄不能为负数'],
    max: [150, '年龄过大'],
    validate: {
      validator: Number.isInteger,
      message: '{VALUE} 不是整数'
    }
  }
});

9.3 环境配置

// config/database.js
const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    const options = {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      useCreateIndex: true,
      useFindAndModify: false
    };

    const conn = await mongoose.connect(process.env.MONGODB_URI, options);

    console.log(`MongoDB 已连接: ${conn.connection.host}`);

    // 开发环境启用调试
    if (process.env.NODE_ENV === 'development') {
      mongoose.set('debug', true);
    }

  } catch (err) {
    console.error('MongoDB 连接失败:', err);
    process.exit(1);
  }
};

module.exports = connectDB;

10. 总结

核心要点

  1. MongoDB 是文档型数据库: 使用 JSON 格式,无固定模式
  2. Mongoose 提供 ODM: 模式定义、验证、类型转换
  3. 灵活的查询: 丰富的查询操作符和聚合功能
  4. 关系处理: 支持嵌入和引用两种方式
  5. 性能优化: 索引、精简查询、批量操作

常用命令

// 连接
mongoose.connect(url, options)

// CRUD
Model.create(data)
Model.find(query)
Model.findOne(query)
Model.findById(id)
Model.updateOne(filter, update)
Model.deleteOne(filter)

// 高级
Model.aggregate(pipeline)
Model.populate(path)
Model.countDocuments(filter)

进阶学习

  • 事务处理 (Transactions)
  • 分片 (Sharding)
  • 复制集 (Replica Sets)
  • 地理空间查询
  • GridFS 文件存储
  • Change Streams
转载请说明出处内容投诉
CSS教程网 » Node.js-MongoDB数据库

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买