数据库集成:MongoDB

MongoDB 是最流行的 NoSQL 数据库之一,以文档(Document)形式存储数据,与 Node.js 结合非常紧密。本章将介绍如何使用 Mongoose ODM(对象文档映射器)在 Node.js 应用中连接和操作 MongoDB,从安装到高级查询,帮助你快速掌握数据持久化技能。

1. MongoDB 简介

MongoDB 是一个基于分布式文件存储的数据库,使用 BSON(类 JSON)格式存储数据。它的主要特点包括:

  • 文档模型:数据以文档形式存储,结构灵活,无需预定义表结构。
  • 高可扩展性:支持分片,可以水平扩展。
  • 丰富的查询语言:支持 CRUD、聚合、地理空间查询等。
  • 索引支持:可创建各种索引提升查询性能。

在 Node.js 生态中,mongoose 是操作 MongoDB 最常用的库,它提供了 Schema 验证、中间件、虚拟属性等方便的功能。

2. 环境准备

在开始之前,请确保你的系统已安装 MongoDB 数据库,并启动服务。可以从 MongoDB 官网 下载安装。或者使用云数据库(如 MongoDB Atlas)获取连接字符串。

创建项目目录并初始化 package.json

mkdir node-mongodb
cd node-mongodb
npm init -y

3. 安装 Mongoose

Mongoose 是一个优雅的 MongoDB ODM,它提供了 Schema 验证、中间件等功能。安装:

npm install mongoose

同时可以安装 dotenv 管理环境变量(可选):

npm install dotenv

4. 连接 MongoDB

在项目根目录创建 .env 文件,存放数据库连接字符串:

MONGODB_URI=mongodb://localhost:27017/mydb

然后创建 db.js 或直接在应用入口连接:

const mongoose = require('mongoose');
require('dotenv').config();

const connectDB = async () => {
  try {
    await mongoose.connect(process.env.MONGODB_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    console.log('MongoDB 连接成功');
  } catch (err) {
    console.error('MongoDB 连接失败:', err.message);
    process.exit(1);
  }
};

module.exports = connectDB;

app.jsserver.js 中调用:

const express = require('express');
const connectDB = require('./db');

const app = express();
connectDB();

// ... 其他中间件和路由
提示: useNewUrlParseruseUnifiedTopology 是 Mongoose 推荐的选项,用于解决旧的连接解析器问题。

5. 定义 Schema 和 Model

在 Mongoose 中,一切从 Schema 开始。Schema 定义了文档的结构、默认值、验证器等。然后通过 mongoose.model 方法编译成 Model。

创建一个 models/User.js 文件:

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true, '姓名不能为空'],
    trim: true,
    maxlength: [50, '姓名不能超过50个字符']
  },
  email: {
    type: String,
    required: [true, '邮箱不能为空'],
    unique: true,
    lowercase: true,
    match: [/^\S+@\S+\.\S+$/, '请提供有效的邮箱地址']
  },
  age: {
    type: Number,
    min: [0, '年龄不能小于0'],
    max: [120, '年龄不能超过120']
  },
  role: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user'
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

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

这里我们定义了一个 User 模型,包含了常见的字段和验证规则。注意 unique 选项不是验证器,而是用于创建数据库索引。

6. CRUD 操作

有了 Model,就可以进行增删改查操作。以下示例均使用 async/await 语法。

6.1 创建文档 (Create)

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

const createUser = async (userData) => {
  try {
    const user = new User(userData);
    const savedUser = await user.save();
    console.log('用户创建成功:', savedUser);
    return savedUser;
  } catch (err) {
    console.error('创建用户失败:', err.message);
  }
};

// 调用
createUser({ name: '张三', email: 'zhangsan@example.com', age: 25 });

也可以使用 User.create(userData) 直接创建并保存。

6.2 查询文档 (Read)

查找所有用户:

const getAllUsers = async () => {
  const users = await User.find();
  console.log(users);
};

条件查询:

const getAdults = async () => {
  const adults = await User.find({ age: { $gte: 18 } });
  console.log(adults);
};

查找单个用户(通过 ID):

const getUserById = async (id) => {
  const user = await User.findById(id);
  console.log(user);
};

查询一条记录:

const getUserByEmail = async (email) => {
  const user = await User.findOne({ email });
  console.log(user);
};

6.3 更新文档 (Update)

通过 ID 更新:

const updateUser = async (id, updateData) => {
  const user = await User.findByIdAndUpdate(id, updateData, {
    new: true,        // 返回更新后的文档
    runValidators: true // 运行验证器
  });
  console.log('更新后的用户:', user);
  return user;
};

或者先查找再修改:

const user = await User.findById(id);
user.name = '新名字';
await user.save();

6.4 删除文档 (Delete)

const deleteUser = async (id) => {
  const result = await User.findByIdAndDelete(id);
  if (result) {
    console.log('用户删除成功');
  } else {
    console.log('用户不存在');
  }
};

也可使用 deleteOnedeleteMany

7. 高级查询与聚合

MongoDB 提供了丰富的查询操作符和聚合管道,用于复杂数据分析。

7.1 查询操作符示例

  • $lt, $lte, $gt, $gte:比较运算符
  • $in, $nin:在/不在数组中
  • $or, $and:逻辑运算
  • $regex:正则表达式
// 查询年龄在20到30之间的用户
const users = await User.find({ age: { $gte: 20, $lte: 30 } });

// 查询名字包含 '张' 的用户
const users = await User.find({ name: { $regex: '张', $options: 'i' } });

7.2 聚合管道

聚合框架用于数据统计和转换。例如,统计每个角色的用户数量:

const stats = await User.aggregate([
  { $group: { _id: '$role', count: { $sum: 1 } } }
]);
console.log(stats); // [ { _id: 'user', count: 10 }, { _id: 'admin', count: 2 } ]

更复杂的聚合可以包括 $match$project$lookup(关联)等阶段。

8. 数据验证

Mongoose 内置了丰富的验证器,如 requiredmin/maxenummatch 等。你也可以编写自定义验证器:

const userSchema = new mongoose.Schema({
  phone: {
    type: String,
    validate: {
      validator: function(v) {
        return /\d{11}/.test(v);
      },
      message: props => `${props.value} 不是有效的11位手机号`
    }
  }
});

9. 索引

为了提高查询性能,可以在 Schema 中定义索引:

userSchema.index({ email: 1 }, { unique: true }); // 创建唯一索引
userSchema.index({ age: -1 }); // 降序索引

或者在字段定义中使用 index: true。索引可以在应用启动时自动创建(需要设置 autoIndex,生产环境建议手动管理)。

10. 模型关联 (Population)

MongoDB 本身不是关系型数据库,但 Mongoose 提供了 populate 方法来实现类似外键的关联。例如,一个文章(Post)属于一个用户:

const postSchema = new mongoose.Schema({
  title: String,
  content: String,
  author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }
});

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

// 查询文章并填充作者信息
const posts = await Post.find().populate('author');
console.log(posts[0].author.name); // 作者姓名

11. 中间件 (Middleware)

Mongoose 中间件(也称为钩子)可以在某些操作(如 savefind)前后执行逻辑。例如,在保存前对密码进行哈希:

userSchema.pre('save', async function(next) {
  // this 指向当前文档
  if (this.isModified('password')) {
    this.password = await bcrypt.hash(this.password, 10);
  }
  next();
});

12. 使用环境变量管理配置

将数据库连接字符串、密码等敏感信息存储在环境变量中(如 .env),并使用 dotenv 加载。示例:

require('dotenv').config();

const mongoose = require('mongoose');

mongoose.connect(process.env.MONGODB_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true
});

13. 完整示例:构建用户 API

下面是一个使用 Express + Mongoose 的简单 REST API 示例:

const express = require('express');
const connectDB = require('./db');
const User = require('./models/User');

const app = express();
connectDB();

app.use(express.json());

// 创建用户
app.post('/users', async (req, res) => {
  try {
    const user = new User(req.body);
    await user.save();
    res.status(201).json(user);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// 获取所有用户
app.get('/users', async (req, res) => {
  try {
    const users = await User.find();
    res.json(users);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// 获取单个用户
app.get('/users/:id', async (req, res) => {
  try {
    const user = await User.findById(req.params.id);
    if (!user) return res.status(404).json({ error: '用户不存在' });
    res.json(user);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// 更新用户
app.put('/users/:id', async (req, res) => {
  try {
    const user = await User.findByIdAndUpdate(req.params.id, req.body, {
      new: true,
      runValidators: true
    });
    if (!user) return res.status(404).json({ error: '用户不存在' });
    res.json(user);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// 删除用户
app.delete('/users/:id', async (req, res) => {
  try {
    const user = await User.findByIdAndDelete(req.params.id);
    if (!user) return res.status(404).json({ error: '用户不存在' });
    res.json({ message: '用户删除成功' });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

14. 最佳实践与注意事项

  • 连接管理:在应用启动时连接一次,在整个生命周期中复用该连接,避免频繁创建新连接。
  • 错误处理:总是使用 try/catch 捕获数据库操作的异常,避免进程崩溃。
  • 验证:在 Schema 中定义验证规则,而不是只在业务逻辑中验证。
  • 索引:为常用查询字段创建索引,提高性能。但不要过度索引。
  • 数据安全:将敏感信息(如密码)哈希存储,不要在查询中返回。
  • 日志:在开发时可以启用 Mongoose 调试模式:mongoose.set('debug', true);
  • 迁移:对于数据结构变化,可以考虑使用迁移脚本(如 migrate-mongo)。

15. 故障排查

  • 连接失败:检查 MongoDB 服务是否运行,连接字符串是否正确,网络是否可达。
  • 验证错误:仔细查看错误信息,确认数据类型和约束条件。
  • 唯一索引冲突:确保插入的数据在唯一字段上没有重复。
总结: 本章介绍了使用 Mongoose 在 Node.js 中集成 MongoDB 的完整流程,从连接、定义模型到执行 CRUD 操作和高级查询。MongoDB 的灵活性和 Mongoose 的便利性使得 Node.js 开发效率极高。在下一章中,我们将探讨如何将关系型数据库(如 MySQL)集成到 Node.js 应用中。