1. MySQL 在 Egg.js 中的深度集成与实践
作为现代 Web 开发的核心组件,数据库操作是每个开发者必须掌握的技能。在 Egg.js 框架中,官方提供的 egg-mysql 插件为我们提供了优雅的 MySQL 数据库访问方案。不同于简单的数据库连接,我们需要从工程化角度考虑配置管理、操作封装和性能优化等问题。
1.1 环境准备与插件配置
在开始之前,确保你的开发环境已经安装并运行了 MySQL 服务(推荐 5.7+ 版本)。我建议使用 Docker 来快速搭建开发环境:
bash复制docker run --name mysql-dev -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -d mysql:5.7
安装 egg-mysql 插件时,我强烈建议锁定版本号以避免潜在的兼容性问题:
bash复制npm i --save egg-mysql@3.0.0
在插件配置环节,很多新手会忽略 app 和 agent 这两个关键参数的区别:
app: true表示将 mysql 实例挂载到 app 对象上agent: false表示不在多进程模式下使用 agent 进程管理连接
提示:在生产环境中,建议将数据库配置放在独立的配置文件中,并通过环境变量注入敏感信息,切勿将密码直接硬编码在代码中。
1.2 多环境配置策略
实际项目中,我们需要为不同环境(开发、测试、生产)配置不同的数据库连接。Egg.js 的配置文件体系完美支持这种需求:
javascript复制// config/config.default.js
exports.mysql = {
client: {
host: '127.0.0.1',
port: '3306',
user: 'root',
password: process.env.MYSQL_PASSWORD || 'dev_password',
database: 'test_dev'
}
};
// config/config.prod.js
exports.mysql = {
client: {
host: process.env.MYSQL_HOST || 'mysql.prod.com',
port: process.env.MYSQL_PORT || '3306',
user: process.env.MYSQL_USER || 'prod_user',
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_DATABASE || 'app_prod'
}
};
对于多数据源场景,我推荐使用命名空间来区分不同业务模块的数据库访问:
javascript复制exports.mysql = {
clients: {
userDB: {
host: 'user.db.service',
// ...其他配置
},
orderDB: {
host: 'order.db.service',
// ...其他配置
}
},
default: {
// 默认配置
}
};
1.3 高级连接池配置
egg-mysql 底层使用 mysql2 驱动,我们可以通过 pool 参数优化连接池性能:
javascript复制exports.mysql = {
client: {
// ...基础配置
pool: {
max: 20, // 最大连接数
min: 3, // 最小连接数
acquireTimeout: 30000, // 获取连接超时时间(ms)
idleTimeout: 10000 // 连接空闲超时时间(ms)
},
connectionLimit: 10, // 单连接限制
queueLimit: 1000 // 排队请求限制
}
};
经验分享:根据我的实践,对于常规应用,max 设置为 CPU 核心数的 5-10 倍是比较合理的。过大的连接数反而会导致性能下降。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库操作最佳实践
2.1 Service 层封装模式
在 Egg.js 中,我们应该严格遵循分层架构原则,将数据库操作封装在 Service 层。这里展示一个完整的用户服务示例:
javascript复制// app/service/user.js
const { Service } = require('egg');
class UserService extends Service {
// 创建用户
async create(userData) {
const { ctx } = this;
try {
const result = await ctx.app.mysql.insert('users', {
...userData,
created_at: ctx.app.mysql.literals.now,
updated_at: ctx.app.mysql.literals.now
});
if (result.affectedRows !== 1) {
ctx.throw(500, '用户创建失败');
}
return result.insertId;
} catch (e) {
if (e.code === 'ER_DUP_ENTRY') {
ctx.throw(409, '用户名已存在');
}
ctx.throw(500, '数据库操作异常');
}
}
// 分页查询
async list({ page = 1, pageSize = 10, where = {} }) {
const { ctx } = this;
const offset = (page - 1) * pageSize;
const [users, total] = await Promise.all([
ctx.app.mysql.select('users', {
where,
limit: pageSize,
offset,
orders: [['id', 'desc']]
}),
ctx.app.mysql.count('users', where)
]);
return {
list: users,
pagination: {
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize)
}
};
}
}
2.2 事务处理实战
数据库事务是保证数据一致性的关键。egg-mysql 提供了两种事务处理方式:
自动提交/回滚:
javascript复制async updateUserInfo(userId, info) {
const { ctx } = this;
// 自动处理事务
return ctx.app.mysql.beginTransactionScope(async conn => {
// 更新基本信息
await conn.update('users', {
id: userId,
...info,
updated_at: conn.literals.now
});
// 更新扩展信息
await conn.update('user_profiles', {
user_id: userId,
...info.profile
});
return true;
}, ctx);
}
手动控制事务:
javascript复制async transferMoney(from, to, amount) {
const { ctx } = this;
const conn = await ctx.app.mysql.beginTransaction();
try {
// 扣款
await conn.query(
'UPDATE accounts SET balance = balance - ? WHERE user_id = ? AND balance >= ?',
[amount, from, amount]
);
// 存款
await conn.query(
'UPDATE accounts SET balance = balance + ? WHERE user_id = ?',
[amount, to]
);
await conn.commit();
return true;
} catch (err) {
await conn.rollback();
ctx.throw(500, '转账失败');
}
}
2.3 复杂查询与性能优化
对于复杂查询场景,我们需要特别注意 SQL 性能:
javascript复制async getPostsWithAuthors(limit = 10) {
const { ctx } = this;
// 使用JOIN替代多次查询
const sql = `
SELECT
p.id, p.title, p.content,
u.id AS author_id, u.name AS author_name,
COUNT(c.id) AS comment_count
FROM posts p
LEFT JOIN users u ON p.author_id = u.id
LEFT JOIN comments c ON p.id = c.post_id
GROUP BY p.id
ORDER BY p.created_at DESC
LIMIT ?
`;
return ctx.app.mysql.query(sql, [limit]);
}
性能提示:对于高频访问的热点数据,建议在 Service 层实现缓存逻辑。可以使用 Egg.js 的缓存插件或直接集成 Redis。
3. RESTful API 设计与实现
3.1 核心设计原则
真正的 RESTful API 不仅仅是 URL 设计,更是一套完整的架构风格。我的实践总结出以下关键点:
- 资源导向:所有端点都是名词(如
/articles),动词通过 HTTP 方法表达 - 状态码语义化:正确使用 2xx/4xx/5xx 状态码
- HATEOAS:在响应中包含相关操作链接(进阶)
- 版本控制:通过 URL 路径或 Header 实现
3.2 完整路由配置示例
javascript复制// app/router.js
module.exports = app => {
const { router, controller } = app;
// API版本控制
router.prefix('/api/v2');
// 标准资源路由
router.resources('users', '/users', controller.users);
router.resources('articles', '/articles', controller.articles);
// 自定义动作路由
router.post('/articles/:id/like', controller.articles.like);
router.post('/articles/:id/collect', controller.articles.collect);
// 批量操作
router.delete('/articles/batch', controller.articles.deleteBatch);
};
3.3 增强型 Controller 实现
javascript复制// app/controller/articles.js
const Controller = require('egg').Controller;
const createRule = {
title: { type: 'string', min: 5, max: 100 },
content: { type: 'string', min: 10 },
tags: { type: 'array', itemType: 'string', required: false }
};
class ArticleController extends Controller {
// 创建文章
async create() {
const { ctx } = this;
// 参数校验
ctx.validate(createRule);
// 组装数据
const payload = {
...ctx.request.body,
author_id: ctx.user.id,
created_at: new Date()
};
// 调用Service
const id = await ctx.service.articles.create(payload);
// 返回响应
ctx.set('Location', `/api/v2/articles/${id}`);
ctx.body = {
data: { id },
links: {
self: `/api/v2/articles/${id}`,
comments: `/api/v2/articles/${id}/comments`
}
};
ctx.status = 201;
}
// 点赞文章
async like() {
const { ctx } = this;
const { id } = ctx.params;
await ctx.service.articles.like(id, ctx.user.id);
ctx.body = {
data: { liked: true },
links: {
article: `/api/v2/articles/${id}`,
unliked: {
href: `/api/v2/articles/${id}/like`,
method: 'DELETE'
}
}
};
}
}
3.4 高级参数校验技巧
Egg.js 的 egg-validate 插件基于 parameter 库,支持复杂的校验规则:
javascript复制// 在Controller中定义校验规则
const updateRule = {
id: { type: 'int', required: true },
title: { type: 'string', min: 5, max: 100, required: false },
content: { type: 'string', min: 10, required: false },
status: {
type: 'enum',
values: ['draft', 'published', 'archived'],
required: false
},
meta: {
type: 'object',
required: false,
rule: {
views: { type: 'int', min: 0, required: false },
likes: { type: 'int', min: 0, required: false }
}
}
};
// 使用校验
ctx.validate(updateRule, {
...ctx.params,
...ctx.request.body
});
对于更复杂的业务校验,我建议创建独立的 Validator 类:
javascript复制// app/validator/article.js
module.exports = app => {
const { validator } = app;
// 添加自定义校验规则
validator.addRule('articleSlug', (rule, value) => {
if (!/^[a-z0-9-]+$/.test(value)) {
return 'slug格式不正确';
}
});
// 复合校验方法
function validatePublish(ctx) {
if (ctx.request.body.status === 'published') {
if (!ctx.request.body.tags || ctx.request.body.tags.length < 1) {
ctx.throw(422, '发布文章必须至少有一个标签');
}
}
}
return { validatePublish };
};
4. 异常处理与日志记录
4.1 结构化错误处理
javascript复制// app/middleware/error_handler.js
module.exports = () => {
return async function errorHandler(ctx, next) {
try {
await next();
// 处理404
if (ctx.status === 404 && !ctx.body) {
ctx.body = {
error: 'Not Found',
documentation_url: 'https://api.example.com/docs'
};
}
} catch (err) {
ctx.app.emit('error', err, ctx);
// 生产环境过滤敏感信息
const isProd = ctx.app.config.env === 'prod';
const status = err.status || 500;
ctx.status = status;
ctx.body = {
error: status === 500 && isProd
? 'Internal Server Error'
: err.message,
code: err.code || 'UNKNOWN_ERROR',
request_id: ctx.state.requestId,
...(status === 422 && { details: err.errors })
};
// 客户端错误不记录完整堆栈
if (status >= 500) {
ctx.app.logger.error(err);
}
}
};
};
4.2 请求日志增强
在 config/config.default.js 中配置自定义日志格式:
javascript复制exports.logger = {
contextFormatter(meta) {
const { level, date, message } = meta;
const { method, url, host, ip } = meta.ctx;
return `[${date}] ${level} ${method} ${url} (${host}) ${ip} - ${message}`;
}
};
对于 API 请求,建议添加请求ID实现全链路追踪:
javascript复制// app/middleware/request_id.js
const { v4: uuidv4 } = require('uuid');
module.exports = () => {
return async function requestId(ctx, next) {
const requestId = ctx.get('X-Request-Id') || uuidv4();
ctx.state.requestId = requestId;
ctx.set('X-Request-Id', requestId);
ctx.app.logger.addContext('requestId', requestId);
await next();
};
};
5. 单元测试与集成测试
5.1 Controller 测试最佳实践
javascript复制// test/controller/articles.test.js
const { app, mock, assert } = require('egg-mock/bootstrap');
describe('GET /api/v2/articles', () => {
it('should return article list', () => {
// Mock Service 层返回
app.mockService('articles', 'list', () => ({
list: [{ id: 1, title: 'Mock Article' }],
pagination: { total: 1, page: 1 }
}));
return app.httpRequest()
.get('/api/v2/articles')
.expect(200)
.expect(res => {
assert(Array.isArray(res.body.data.list));
assert(res.body.data.list[0].title === 'Mock Article');
});
});
it('should validate query params', () => {
return app.httpRequest()
.get('/api/v2/articles?page=0&pageSize=101')
.expect(422)
.expect(res => {
assert(res.body.error === 'Validation Failed');
assert(res.body.details.some(d => d.field === 'page'));
});
});
});
5.2 Service 测试技巧
javascript复制// test/service/articles.test.js
describe('create()', () => {
let ctx;
let service;
beforeEach(() => {
ctx = app.mockContext();
service = ctx.service.articles;
// Mock数据库操作
app.mockMysql('insert', () => ({
affectedRows: 1,
insertId: 123
}));
});
it('should create article with valid data', async () => {
const id = await service.create({
title: 'Test Article',
content: 'This is a test content',
author_id: 1
});
assert(id === 123);
});
it('should throw when title is empty', async () => {
try {
await service.create({
title: '',
content: 'content',
author_id: 1
});
assert.fail('should throw error');
} catch (err) {
assert(err.status === 422);
}
});
});
5.3 集成测试策略
javascript复制// test/api/article.test.js
describe('Article API', () => {
let token;
before(async () => {
// 初始化测试数据
await app.model.User.create({
username: 'tester',
password: '123456'
});
// 获取测试token
const res = await app.httpRequest()
.post('/api/v2/auth/login')
.send({
username: 'tester',
password: '123456'
});
token = res.body.token;
});
it('should create and get article', async () => {
// 创建文章
const createRes = await app.httpRequest()
.post('/api/v2/articles')
.set('Authorization', `Bearer ${token}`)
.send({
title: 'Integration Test',
content: 'This is an integration test'
})
.expect(201);
const articleId = createRes.body.data.id;
// 查询文章
await app.httpRequest()
.get(`/api/v2/articles/${articleId}`)
.expect(200)
.expect(res => {
assert(res.body.data.title === 'Integration Test');
});
});
});
6. 项目部署与性能优化
6.1 生产环境配置
javascript复制// config/config.prod.js
exports.mysql = {
client: {
host: process.env.MYSQL_HOST,
port: process.env.MYSQL_PORT,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_DATABASE,
// 生产环境特定配置
pool: {
max: 30,
min: 5,
acquireTimeout: 30000,
idleTimeout: 10000
},
connectTimeout: 10000,
charset: 'utf8mb4',
supportBigNumbers: true,
bigNumberStrings: false,
timezone: '+08:00'
}
};
// 启用集群模式
exports.cluster = {
listen: {
port: 7001,
hostname: '0.0.0.0'
}
};
6.2 性能监控与调优
建议集成应用性能监控(APM)工具:
javascript复制// app.js
class AppBootHook {
constructor(app) {
this.app = app;
}
async didReady() {
if (this.app.config.env === 'prod') {
// 初始化APM
require('elastic-apm-node').start({
serviceName: 'egg-api',
serverUrl: process.env.APM_SERVER,
captureBody: 'all'
});
// 监控慢查询
this.app.mysql.on('query', ({ sql, executionTime }) => {
if (executionTime > 500) { // 超过500ms的查询
this.app.logger.warn(`Slow query (${executionTime}ms): ${sql}`);
}
});
}
}
}
6.3 安全加固措施
javascript复制// config/config.prod.js
exports.security = {
csrf: {
enable: true,
ignoreJSON: false,
cookieName: 'csrfToken',
sessionName: 'csrfToken',
headerName: 'x-csrf-token'
},
xframe: {
enable: true,
value: 'SAMEORIGIN'
},
csp: {
enable: true,
policy: {
'default-src': "'self'",
'script-src': "'self' 'unsafe-inline' cdn.example.com",
'style-src': "'self' 'unsafe-inline'",
'img-src': "'self' data:"
}
}
};
// 防止SQL注入
exports.mysql = {
client: {
// ...其他配置
queryFormat: function (query, values) {
if (!values) return query;
return query.replace(/\:(\w+)/g, (txt, key) => {
if (values.hasOwnProperty(key)) {
return this.escape(values[key]);
}
return txt;
});
}
}
};
在实际项目开发中,我强烈建议将数据库操作封装为更高级的 Repository 模式,特别是在复杂业务系统中。同时,对于高频访问的接口,应该考虑实现缓存策略,可以使用 Egg.js 的缓存插件或者直接集成 Redis。
