首页 关于 文章 作品集 工具 联系
返回文章列表

Node.js 数据库设计与 MongoDB 最佳实践

MongoDB 是 Node.js 生态中最常用的 NoSQL 数据库。它的文档存储模型灵活高效,但灵活不等于随便设计——好的数据库设计能让项目后期少掉很多头发。本文从实际项目出发,讲清楚 MongoDB 的数据建模、Mongoose Schema 设计、索引优化和数据验证。

一、为什么选 MongoDB?

和传统关系型数据库(MySQL、PostgreSQL)相比,MongoDB 有几个天然优势:

  • 文档模型:数据结构灵活,字段可以随时增减,适合快速迭代的产品
  • JSON 原生:和 Node.js 的 JavaScript 天然契合,无需 ORM 转换
  • 横向扩展:内置分片(Sharding)支持,数据量大了可以直接水平扩容
  • 丰富的查询:支持聚合管道、全文索引、地理空间查询等高级功能

二、数据库连接管理

使用 Mongoose 连接 MongoDB 时,正确管理连接池非常重要:

const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    const conn = await mongoose.connect(process.env.MONGO_URI, {
      maxPoolSize: 10,           // 连接池大小
      minPoolSize: 2,            // 最小连接数
      serverSelectionTimeoutMS: 5000,  // 服务器选择超时
      socketTimeoutMS: 45000,    // Socket 超时
    });

    

    // 监听连接事件
    mongoose.connection.on('disconnected', () => {
      console.warn('MongoDB 连接断开,尝试重连...');
    });

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

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

module.exports = connectDB;

三、Schema 设计原则

3.1 用户模型

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: [true, '用户名不能为空'],
    unique: true,
    trim: true,
    minlength: [2, '用户名至少2个字符'],
    maxlength: [20, '用户名最多20个字符'],
    match: [/^[a-zA-Z0-9_\u4e00-\u9fa5]+$/, '用户名只能包含中英文、数字和下划线'],
  },
  email: {
    type: String,
    required: [true, '邮箱不能为空'],
    unique: true,
    lowercase: true,
    trim: true,
    match: [/^\S+@\S+\.\S+$/, '邮箱格式不正确'],
  },
  password: {
    type: String,
    required: [true, '密码不能为空'],
    minlength: 6,
    select: false,  // 默认查询不返回密码字段
  },
  avatar: {
    type: String,
    default: '',
  },
  role: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user',
  },
  // 软删除
  isDeleted: {
    type: Boolean,
    default: false,
  },
}, {
  timestamps: true,  // 自动添加 createdAt 和 updatedAt
  toJSON: { virtuals: true },    // toJSON 时包含虚拟字段
  toObject: { virtuals: true },
});

// 索引
userSchema.index({ email: 1 });
userSchema.index({ username: 1 });

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

// 实例方法:比较密码
userSchema.methods.matchPassword = async function(enteredPassword) {
  return await bcrypt.compare(enteredPassword, this.password);
};

module.exports = mongoose.model('User', userSchema);

3.2 文章模型(含分页优化)

const postSchema = new mongoose.Schema({
  title: {
    type: String,
    required: [true, '标题不能为空'],
    trim: true,
    maxlength: 100,
  },
  content: {
    type: String,
    required: true,
  },
  summary: {
    type: String,
    maxlength: 200,
  },
  author: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true,
  },
  tags: [{
    type: String,
    lowercase: true,
    trim: true,
  }],
  // 点赞数 —— 用计数器模式避免频繁更新
  likeCount: {
    type: Number,
    default: 0,
  },
  viewCount: {
    type: Number,
    default: 0,
  },
  // 发布状态
  status: {
    type: String,
    enum: ['draft', 'published'],
    default: 'draft',
  },
  isDeleted: {
    type: Boolean,
    default: false,
  },
}, {
  timestamps: true,
});

// 复合索引:状态 + 创建时间(分页查询优化)
postSchema.index({ status: 1, createdAt: -1 });
// 作者索引
postSchema.index({ author: 1 });
// 标签索引
postSchema.index({ tags: 1 });

// 虚拟字段:评论数
postSchema.virtual('commentCount', {
  ref: 'Comment',
  localField: '_id',
  foreignField: 'post',
  count: true,
});

module.exports = mongoose.model('Post', postSchema);

3.3 聊天消息模型

const messageSchema = new mongoose.Schema({
  conversationId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Conversation',
    required: true,
    index: true,
  },
  sender: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true,
  },
  content: {
    type: String,
    required: true,
    trim: true,
  },
  type: {
    type: String,
    enum: ['text', 'image', 'voice', 'system'],
    default: 'text',
  },
  // 消息状态
  status: {
    type: String,
    enum: ['sent', 'delivered', 'read'],
    default: 'sent',
  },
  isDeleted: {
    type: Boolean,
    default: false,
  },
}, {
  timestamps: true,
});

// TTL 索引:30天后自动删除已删除的消息
messageSchema.index(
  { isDeleted: 1, updatedAt: 1 },
  { expireAfterSeconds: 2592000, partialFilterExpression: { isDeleted: true } }
);

module.exports = mongoose.model('Message', messageSchema);

四、聚合管道实战

MongoDB 的聚合管道是处理复杂数据查询的利器。下面展示几个常用场景:

4.1 文章列表带作者信息

const getPosts = async (page = 1, limit = 10) => {
  const posts = await Post.aggregate([
    // 过滤条件
    { $match: { status: 'published', isDeleted: false } },
    // 关联作者
    {
      $lookup: {
        from: 'users',
        localField: 'author',
        foreignField: '_id',
        as: 'authorInfo',
      },
    },
    // 展开作者数组
    { $unwind: { path: '$authorInfo', preserveNullAndEmptyArrays: true } },
    // 排序
    { $sort: { createdAt: -1 } },
    // 分页
    { $skip: (page - 1) * limit },
    { $limit: limit },
    // 字段投影
    {
      $project: {
        title: 1,
        summary: 1,
        tags: 1,
        likeCount: 1,
        viewCount: 1,
        createdAt: 1,
        'authorInfo.username': 1,
        'authorInfo.avatar': 1,
      },
    },
  ]);

  return posts;
};

4.2 统计数据

const getDashboardStats = async (userId) => {
  const stats = await Post.aggregate([
    { $match: { author: mongoose.Types.ObjectId(userId), isDeleted: false } },
    {
      $group: {
        _id: null,
        totalPosts: { $sum: 1 },
        totalViews: { $sum: '$viewCount' },
        totalLikes: { $sum: '$likeCount' },
        avgViews: { $avg: '$viewCount' },
      },
    },
  ]);

  return stats[0] || { totalPosts: 0, totalViews: 0, totalLikes: 0 };
};

五、索引优化

索引是数据库性能的关键。MongoDB 索引和 MySQL 类似,但也有自己的特点:

// 单字段索引
userSchema.index({ email: 1 });

// 复合索引(注意字段顺序)
postSchema.index({ status: 1, createdAt: -1 });

// 文本索引(支持全文搜索)
postSchema.index({ title: 'text', content: 'text' });

// 地理空间索引
locationSchema.index({ coordinates: '2dsphere' });

// 唯一索引
userSchema.index({ email: 1 }, { unique: true });

// 部分索引(只对满足条件的文档建索引)
messageSchema.index(
  { isDeleted: 1 },
  { partialFilterExpression: { isDeleted: false } }
);

// 查看当前集合索引
// db.posts.getIndexes()

// 分析查询是否使用索引
// db.posts.find({ status: 'published' }).explain('executionStats')

六、数据备份与安全

// 导出数据库
// mongodump --uri="mongodb://localhost:27017/myapp" --out=./backup

// 导入数据库
// mongorestore --uri="mongodb://localhost:27017/myapp" ./backup

// 安全配置要点:
// 1. 启用认证:security.authorization: enabled
// 2. 创建最小权限账户
// 3. 禁用默认端口对外暴露
// 4. 启用 SSL/TLS 加密连接
// 5. 定期备份(建议每天)

总结

MongoDB 的灵活文档模型让它特别适合快速迭代的 Node.js 项目。但灵活不等于随意——良好的 Schema 设计、合理的索引策略、完善的数据验证,是项目长期健康运行的基础。希望这篇文章能帮你在实际项目中少踩一些坑。