跳到主要内容

数据库集成

大多数 Web 应用都需要持久化存储数据。本章介绍 Express.js 与常见数据库的集成方法,包括 MongoDB、MySQL 和 PostgreSQL。

数据库选择

在选择数据库时,需要考虑以下因素:

数据库类型适用场景学习曲线
MongoDB文档型灵活数据结构、快速迭代
MySQL关系型结构化数据、事务处理
PostgreSQL关系型复杂查询、数据完整性中高
Redis键值型缓存、会话存储

MongoDB + Mongoose

MongoDB 是 Express 应用最常用的数据库,Mongoose 是最受欢迎的 ODM(对象文档映射)库。

为什么选择 MongoDB?

MongoDB 是文档型数据库,数据以 BSON(二进制 JSON)格式存储。它的特点包括:

  1. 灵活的模式:不需要预先定义表结构,适合快速迭代
  2. 嵌套文档:支持复杂的数据结构,减少表连接
  3. 水平扩展:易于分片和复制
  4. 与 JavaScript 天然契合:JSON 格式与 JavaScript 对象一致

安装与连接

npm install mongoose

连接 MongoDB 有两种方式:使用连接字符串或分开指定参数。

const mongoose = require('mongoose');

// 方式一:使用连接字符串(推荐)
const connectDB = async () => {
try {
// 连接字符串格式:mongodb://[username:password@]host[:port]/database
await mongoose.connect(process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/myapp');
console.log('MongoDB 连接成功');
} catch (err) {
console.error('MongoDB 连接失败:', err.message);
process.exit(1); // 连接失败时退出进程
}
};

connectDB();

重要提示:推荐使用 127.0.0.1 而不是 localhost。Node.js 18+ 优先使用 IPv6,会将 localhost 解析为 ::1,而 MongoDB 默认不启用 IPv6。

连接选项

从 Mongoose 6.0 开始,以下选项默认启用,不需要手动指定:

// Mongoose 6+ 自动应用这些选项
// useNewUrlParser: true
// useUnifiedTopology: true
// useCreateIndex: true
// useFindAndModify: false

常用的连接选项:

await mongoose.connect(uri, {
maxPoolSize: 10, // 最大连接数(默认 100)
minPoolSize: 2, // 最小连接数
serverSelectionTimeoutMS: 5000, // 服务器选择超时(默认 30 秒)
socketTimeoutMS: 45000, // Socket 超时
family: 4 // 强制使用 IPv4
});

连接事件处理

监听连接状态变化,对调试和监控很重要:

const mongoose = require('mongoose');

mongoose.connection.on('connected', () => {
console.log('MongoDB 连接成功');
});

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

mongoose.connection.on('disconnected', () => {
console.log('MongoDB 断开连接');
});

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

定义 Schema 和 Model

Schema 定义文档的结构,Model 是操作数据库的接口。

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const userSchema = new Schema({
username: {
type: String,
required: [true, '用户名不能为空'],
unique: true,
trim: true,
minlength: [3, '用户名至少 3 个字符'],
maxlength: [20, '用户名最多 20 个字符']
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, '请输入有效的邮箱地址']
},
password: {
type: String,
required: true,
minlength: 6,
select: false // 查询时不返回密码字段
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
avatar: String,
bio: {
type: String,
maxlength: 200
},
isActive: {
type: Boolean,
default: true
},
lastLoginAt: Date
}, {
timestamps: true, // 自动添加 createdAt 和 updatedAt
versionKey: false // 不添加 __v 字段
});

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

Schema 类型

Mongoose 支持以下数据类型:

类型说明示例
String字符串'hello'
Number数字123
Boolean布尔值true
Date日期new Date()
Buffer二进制数据Buffer.from('test')
ObjectIdMongoDB IDmongoose.Types.ObjectId()
Array数组['a', 'b']
Map键值对new Map([['key', 'value']])
Schema.Types.Mixed任意类型{ any: 'thing' }
Schema.Types.Decimal128高精度小数123.456

中间件(钩子)

Mongoose 中间件可以在文档保存前后执行代码:

const bcrypt = require('bcryptjs');

// 保存前加密密码
userSchema.pre('save', async function(next) {
// 只在密码被修改时才加密
if (!this.isModified('password')) {
return next();
}

// 加密密码
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});

// 实例方法:验证密码
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};

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

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

CRUD 操作

// 创建
const createUser = async (req, res) => {
try {
const user = await User.create(req.body);
res.status(201).json(user);
} catch (err) {
if (err.code === 11000) {
return res.status(400).json({ error: '邮箱或用户名已存在' });
}
res.status(400).json({ error: err.message });
}
};

// 查询列表(带分页)
const getUsers = async (req, res) => {
const { page = 1, limit = 10, sort = '-createdAt', search } = req.query;

// 构建查询条件
const query = {};
if (search) {
query.$or = [
{ username: { $regex: search, $options: 'i' } },
{ email: { $regex: search, $options: 'i' } }
];
}

// 并行查询数据
const [users, total] = await Promise.all([
User.find(query)
.select('-password')
.sort(sort)
.skip((page - 1) * limit)
.limit(parseInt(limit)),
User.countDocuments(query)
]);

res.json({
data: users,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
};

// 查询单个
const getUser = async (req, res) => {
const user = await User.findById(req.params.id).select('-password');

if (!user) {
return res.status(404).json({ error: '用户不存在' });
}

res.json(user);
};

// 更新
const updateUser = async (req, res) => {
// 使用 new: true 返回更新后的文档
// runValidators: true 运行验证器
const user = await User.findByIdAndUpdate(
req.params.id,
{ $set: req.body },
{ new: true, runValidators: true }
).select('-password');

if (!user) {
return res.status(404).json({ error: '用户不存在' });
}

res.json(user);
};

// 删除
const deleteUser = async (req, res) => {
const user = await User.findByIdAndDelete(req.params.id);

if (!user) {
return res.status(404).json({ error: '用户不存在' });
}

res.status(204).send();
};

关联查询

使用 populate() 进行关联查询:

const postSchema = new Schema({
title: { type: String, required: true },
content: String,
author: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
comments: [{
user: { type: Schema.Types.ObjectId, ref: 'User' },
content: String,
createdAt: { type: Date, default: Date.now }
}]
});

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

// 创建文章时关联用户
const createPost = async (req, res) => {
const post = await Post.create({
...req.body,
author: req.user.id // 从认证中间件获取
});
res.status(201).json(post);
};

// 查询时填充关联数据
const getPost = async (req, res) => {
const post = await Post.findById(req.params.id)
.populate('author', 'username email avatar') // 只选择指定字段
.populate('comments.user', 'username avatar');

res.json(post);
};

MySQL + Sequelize

对于需要事务和复杂关系查询的场景,MySQL 是更好的选择。

安装配置

npm install sequelize mysql2

连接数据库

const { Sequelize } = require('sequelize');

// 方式一:通过连接字符串
const sequelize = new Sequelize('mysql://user:password@localhost:3306/myapp');

// 方式二:通过参数配置
const sequelize = new Sequelize(
process.env.DB_NAME,
process.env.DB_USER,
process.env.DB_PASSWORD,
{
host: process.env.DB_HOST,
dialect: 'mysql',
logging: false, // 关闭 SQL 日志
pool: {
max: 10,
min: 0,
acquire: 30000,
idle: 10000
}
}
);

// 测试连接
const testConnection = async () => {
try {
await sequelize.authenticate();
console.log('MySQL 连接成功');
} catch (err) {
console.error('MySQL 连接失败:', err);
}
};

testConnection();

定义模型

const { DataTypes, Model } = require('sequelize');

class User extends Model {}

User.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
username: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true
},
email: {
type: DataTypes.STRING(100),
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
password: {
type: DataTypes.STRING(255),
allowNull: false
},
role: {
type: DataTypes.ENUM('user', 'admin'),
defaultValue: 'user'
}
}, {
sequelize,
modelName: 'User',
tableName: 'users',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at'
});

CRUD 操作

// 创建
const user = await User.create({
username: 'john',
email: '[email protected]',
password: 'hashedpassword'
});

// 查询所有
const users = await User.findAll({
attributes: ['id', 'username', 'email'], // 选择字段
where: { role: 'user' }, // 条件
order: [['created_at', 'DESC']], // 排序
limit: 10,
offset: 0
});

// 分页查询
const { count, rows } = await User.findAndCountAll({
where: { isActive: true },
limit: parseInt(limit),
offset: (page - 1) * limit
});

// 更新
await User.update(
{ role: 'admin' },
{ where: { id: 1 } }
);

// 删除
await User.destroy({ where: { id: 1 } });

PostgreSQL + Prisma

Prisma 是现代的 ORM,提供类型安全和优秀的开发体验。

安装配置

npm install prisma @prisma/client
npx prisma init

Schema 定义

prisma/schema.prisma 中定义数据模型:

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

generator client {
provider = "prisma-client-js"
}

model User {
id Int @id @default(autoincrement())
email String @unique
username String @unique
password String
role Role @default(USER)
posts Post[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

@@map("users")
}

model Post {
id Int @id @default(autoincrement())
title String
content String?
authorId Int @map("author_id")
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

@@map("posts")
}

enum Role {
USER
ADMIN
}

使用 Prisma Client

const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

// 创建用户
const user = await prisma.user.create({
data: {
email: '[email protected]',
username: 'john',
password: 'hashedpassword'
}
});

// 查询用户(包含文章)
const userWithPosts = await prisma.user.findUnique({
where: { id: 1 },
include: { posts: true }
});

// 分页查询
const [users, total] = await Promise.all([
prisma.user.findMany({
skip: (page - 1) * limit,
take: parseInt(limit),
orderBy: { createdAt: 'desc' }
}),
prisma.user.count()
]);

// 更新用户
await prisma.user.update({
where: { id: 1 },
data: { role: 'ADMIN' }
});

// 删除用户
await prisma.user.delete({
where: { id: 1 }
});

Redis 缓存

Redis 常用于缓存、会话存储和消息队列。

安装与连接

npm install redis
const redis = require('redis');

const client = redis.createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379'
});

client.on('error', (err) => console.error('Redis 错误:', err));
client.on('connect', () => console.log('Redis 连接成功'));

await client.connect();

基本操作

// 设置值(带过期时间)
await client.set('key', 'value', { EX: 3600 }); // 1小时过期

// 获取值
const value = await client.get('key');

// 删除
await client.del('key');

// 检查是否存在
const exists = await client.exists('key');

// 自增
await client.incr('counter');

// 设置哈希
await client.hSet('user:1', {
name: 'John',
email: '[email protected]'
});

// 获取哈希
const user = await client.hGetAll('user:1');

缓存中间件

const cache = (key, ttl = 3600) => {
return async (req, res, next) => {
const cacheKey = `${key}:${req.originalUrl}`;

try {
const cached = await client.get(cacheKey);

if (cached) {
return res.json(JSON.parse(cached));
}

// 保存原始 res.json
const originalJson = res.json.bind(res);

// 重写 res.json
res.json = async (data) => {
await client.set(cacheKey, JSON.stringify(data), { EX: ttl });
return originalJson(data);
};

next();
} catch (err) {
next();
}
};
};

// 使用
app.get('/users', cache('users', 300), getUsers);

数据库连接最佳实践

1. 使用环境变量

# MongoDB
MONGODB_URI=mongodb://username:password@localhost:27017/myapp

# MySQL
DB_HOST=localhost
DB_PORT=3306
DB_NAME=myapp
DB_USER=root
DB_PASSWORD=password

# Redis
REDIS_URL=redis://localhost:6379

2. 连接池配置

连接池大小需要根据应用负载调整:

// MongoDB
await mongoose.connect(uri, {
maxPoolSize: 50, // 高并发应用可以增加
minPoolSize: 5
});

// MySQL/Sequelize
const sequelize = new Sequelize(db, user, pass, {
pool: {
max: 20,
min: 5,
acquire: 30000,
idle: 10000
}
});

3. 优雅关闭

const gracefulShutdown = async () => {
console.log('正在关闭数据库连接...');

await mongoose.connection.close();
await sequelize.close();
await redisClient.quit();

console.log('数据库连接已关闭');
process.exit(0);
};

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

4. 错误处理

// 全局数据库错误处理
app.use((err, req, res, next) => {
// MongoDB 重复键错误
if (err.code === 11000) {
return res.status(400).json({
error: '资源已存在',
field: Object.keys(err.keyValue)[0]
});
}

// Mongoose 验证错误
if (err.name === 'ValidationError') {
const messages = Object.values(err.errors).map(e => e.message);
return res.status(400).json({ error: messages.join(', ') });
}

// 数据库连接错误
if (err.name === 'MongoNetworkError') {
return res.status(503).json({ error: '数据库服务不可用' });
}

next(err);
});

小结

本章介绍了 Express.js 与主流数据库的集成方法:

  1. MongoDB + Mongoose:文档型数据库,灵活的 Schema,适合快速迭代
  2. MySQL + Sequelize:关系型数据库,适合需要事务的场景
  3. PostgreSQL + Prisma:现代 ORM,类型安全,开发体验好
  4. Redis:缓存和会话存储,提升性能

选择数据库时,要根据项目需求、团队能力和运维成本综合考虑。

练习

  1. 使用 Mongoose 创建一个博客系统的数据模型(用户、文章、评论)
  2. 实现一个带缓存的用户 API,使用 Redis 缓存查询结果
  3. 使用 Prisma 创建一个任务管理系统的数据模型
  4. 实现数据库连接的优雅关闭逻辑