数据库集成
大多数 Web 应用都需要持久化存储数据。本章介绍 Express.js 与常见数据库的集成方法,包括 MongoDB、MySQL 和 PostgreSQL。
数据库选择
在选择数据库时,需要考虑以下因素:
| 数据库 | 类型 | 适用场景 | 学习曲线 |
|---|---|---|---|
| MongoDB | 文档型 | 灵活数据结构、快速迭代 | 低 |
| MySQL | 关系型 | 结构化数据、事务处理 | 中 |
| PostgreSQL | 关系型 | 复杂查询、数据完整性 | 中高 |
| Redis | 键值型 | 缓存、会话存储 | 低 |
MongoDB + Mongoose
MongoDB 是 Express 应用最常用的数据库,Mongoose 是最受欢迎的 ODM(对象文档映射)库。
为什么选择 MongoDB?
MongoDB 是文档型数据库,数据以 BSON(二进制 JSON)格式存储。它的特点包括:
- 灵活的模式:不需要预先定义表结构,适合快速迭代
- 嵌套文档:支持复杂的数据结构,减少表连接
- 水平扩展:易于分片和复制
- 与 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') |
ObjectId | MongoDB ID | mongoose.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 与主流数据库的集成方法:
- MongoDB + Mongoose:文档型数据库,灵活的 Schema,适合快速迭代
- MySQL + Sequelize:关系型数据库,适合需要事务的场景
- PostgreSQL + Prisma:现代 ORM,类型安全,开发体验好
- Redis:缓存和会话存储,提升性能
选择数据库时,要根据项目需求、团队能力和运维成本综合考虑。
练习
- 使用 Mongoose 创建一个博客系统的数据模型(用户、文章、评论)
- 实现一个带缓存的用户 API,使用 Redis 缓存查询结果
- 使用 Prisma 创建一个任务管理系统的数据模型
- 实现数据库连接的优雅关闭逻辑