Node.js 数据库操作
Node.js 中数据库连接管理、ORM 选型与性能优化
目录
连接管理
原生 mysql2 连接池
typescript
import mysql from 'mysql2/promise';
// 创建连接池
const pool = mysql.createPool({
host: 'localhost',
port: 3306,
user: 'root',
password: 'password',
database: 'mydb',
// 连接池配置
connectionLimit: 10, // 最大连接数
queueLimit: 0, // 等待队列无限制
waitForConnections: true, // 无可用连接时等待
// 连接配置
connectTimeout: 10000, // 连接超时 10s
enableKeepAlive: true, // 保持连接
keepAliveInitialDelay: 10000
});
// 使用连接池查询
async function query<T>(sql: string, params?: any[]): Promise<T[]> {
const [rows] = await pool.execute(sql, params);
return rows as T[];
}
// 获取单个连接(用于事务)
async function getConnection() {
return pool.getConnection();
}
// 优雅关闭
process.on('SIGTERM', async () => {
await pool.end();
process.exit(0);
});预处理语句防 SQL 注入
typescript
// ✅ 正确:使用参数化查询
const [users] = await pool.execute(
'SELECT * FROM users WHERE id = ? AND status = ?',
[userId, 'active']
);
// ❌ 危险:字符串拼接
const sql = `SELECT * FROM users WHERE id = ${userId}`; // SQL 注入风险ORM 选型
方案对比
| ORM | 类型 | 特点 | 适用场景 |
|---|---|---|---|
| Prisma | Schema-first | 类型安全、迁移工具完善 | 新项目首选 |
| TypeORM | Code-first | 装饰器语法、成熟 | 传统 ORM 用户 |
| Drizzle | Schema-first | 轻量、SQL-like API | 性能敏感场景 |
| Knex.js | Query Builder | 灵活、无 ORM 开销 | 复杂查询 |
Prisma 实践
prisma
// schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
}typescript
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// 查询
const users = await prisma.user.findMany({
where: { email: { contains: '@example.com' } },
include: { posts: true },
orderBy: { createdAt: 'desc' },
take: 10
});
// 创建
const user = await prisma.user.create({
data: {
email: 'user@example.com',
name: 'John',
posts: {
create: [
{ title: 'First Post' }
]
}
}
});
// 更新
await prisma.user.update({
where: { id: 1 },
data: { name: 'Jane' }
});
// 删除
await prisma.user.delete({ where: { id: 1 } });TypeORM 实践
typescript
import { Entity, Column, PrimaryGeneratedColumn, OneToMany, ManyToOne } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
@Column({ nullable: true })
name: string;
@OneToMany(() => Post, post => post.author)
posts: Post[];
}
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@ManyToOne(() => User, user => user.posts)
author: User;
}
// Repository 模式
const userRepository = dataSource.getRepository(User);
const users = await userRepository.find({
where: { email: Like('%@example.com') },
relations: ['posts'],
order: { id: 'DESC' },
take: 10
});事务处理
原生事务
typescript
async function transferMoney(fromId: number, toId: number, amount: number) {
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
// 扣款
await connection.execute(
'UPDATE accounts SET balance = balance - ? WHERE id = ?',
[amount, fromId]
);
// 入账
await connection.execute(
'UPDATE accounts SET balance = balance + ? WHERE id = ?',
[amount, toId]
);
await connection.commit();
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
}Prisma 事务
typescript
// 交互式事务
await prisma.$transaction(async (tx) => {
const from = await tx.account.update({
where: { id: fromId },
data: { balance: { decrement: amount } }
});
if (from.balance < 0) {
throw new Error('Insufficient balance');
}
await tx.account.update({
where: { id: toId },
data: { balance: { increment: amount } }
});
});
// 批量事务
await prisma.$transaction([
prisma.user.create({ data: { email: 'a@test.com' } }),
prisma.user.create({ data: { email: 'b@test.com' } })
]);事务隔离级别
| 级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| READ UNCOMMITTED | ✅ | ✅ | ✅ | 最低隔离 |
| READ COMMITTED | ❌ | ✅ | ✅ | 多数 DB 默认 |
| REPEATABLE READ | ❌ | ❌ | ✅ | MySQL 默认 |
| SERIALIZABLE | ❌ | ❌ | ❌ | 完全隔离 |
typescript
// Prisma 设置隔离级别
await prisma.$transaction(async (tx) => {
// ...
}, {
isolationLevel: 'Serializable'
});性能优化
N+1 问题
typescript
// ❌ N+1 问题
const posts = await prisma.post.findMany();
for (const post of posts) {
const author = await prisma.user.findUnique({
where: { id: post.authorId }
});
}
// ✅ 使用 include 一次加载
const posts = await prisma.post.findMany({
include: { author: true }
});
// ✅ 使用 select 只取需要的字段
const posts = await prisma.post.findMany({
select: {
id: true,
title: true,
author: { select: { name: true } }
}
});索引优化
sql
-- 高频查询字段加索引
CREATE INDEX idx_user_email ON users(email);
CREATE INDEX idx_post_author ON posts(author_id);
-- 复合索引(左匹配原则)
CREATE INDEX idx_order_user_status ON orders(user_id, status, created_at);慢查询分析
sql
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;
-- 查看执行计划
EXPLAIN SELECT * FROM users WHERE email LIKE '%@test.com';批量操作
typescript
// ❌ 逐条插入
for (const user of users) {
await prisma.user.create({ data: user });
}
// ✅ 批量插入
await prisma.user.createMany({
data: users,
skipDuplicates: true
});
// ✅ 原生批量插入
const values = users.map(u => `('${u.email}', '${u.name}')`).join(',');
await pool.execute(`INSERT INTO users (email, name) VALUES ${values}`);连接池监控
typescript
import mysql from 'mysql2/promise';
const pool = mysql.createPool({ /* config */ });
// 监控连接池状态
setInterval(() => {
const status = pool.pool;
console.log({
all: status.all.length, // 所有连接
idle: status.idle.length, // 空闲连接
queue: status.queue.length // 等待队列
});
}, 10000);
// 健康检查
async function healthCheck(): Promise<boolean> {
try {
await pool.execute('SELECT 1');
return true;
} catch {
return false;
}
}高频面试题
Q1: 连接池大小如何设置?
经验公式:connections = (core_count * 2) + effective_spindle_count
- CPU 密集型:连接数 ≈ CPU 核心数
- I/O 密集型:可适当增加到 2-4 倍核心数
- 通用推荐:10-20 个连接,根据监控调整
Q2: 如何解决 N+1 查询问题?
- Eager Loading:使用
include/relations预加载关联数据 - DataLoader:批量合并相同类型的查询
- 手动 JOIN:复杂场景使用原生 SQL
typescript
// DataLoader 示例
const userLoader = new DataLoader(async (ids: number[]) => {
const users = await prisma.user.findMany({
where: { id: { in: ids } }
});
return ids.map(id => users.find(u => u.id === id));
});Q3: 事务中如何处理死锁?
- 统一获取锁顺序:按固定顺序访问表/行
- 减少事务时间:快进快出
- 重试机制:捕获死锁异常后重试
typescript
async function withRetry<T>(fn: () => Promise<T>, retries = 3): Promise<T> {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error: any) {
if (error.code === 'ER_LOCK_DEADLOCK' && i < retries - 1) {
await new Promise(r => setTimeout(r, 100 * (i + 1)));
continue;
}
throw error;
}
}
throw new Error('Max retries exceeded');
}Q4: Prisma vs TypeORM 如何选择?
| 维度 | Prisma | TypeORM |
|---|---|---|
| 类型安全 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 学习曲线 | 低 | 中 |
| 原生 SQL | 支持 | 支持 |
| 迁移工具 | 完善 | 一般 |
| 性能 | 好 | 好 |
| 生态 | 新兴 | 成熟 |
最佳实践
- [ ] 使用连接池,避免频繁建立连接
- [ ] 参数化查询防止 SQL 注入
- [ ] 使用
include/relations避免 N+1 - [ ] 为高频查询字段添加索引
- [ ] 事务中做好错误回滚
- [ ] 监控连接池和慢查询
- [ ] 批量操作代替循环单条操作