别再死记硬背SQL了!用Node.js实战项目带你玩转数据库增删改查
别再死记硬背SQL了!用Node.js实战项目带你玩转数据库增删改查
记得刚开始学编程时,最让我头疼的就是SQL语句。那些CREATE TABLE、ALTER COLUMN看起来就像天书,直到有一天导师扔给我一个用户管理系统的需求:"用Node.js实现,必须用原生SQL操作数据库"。在边做边学的过程中,我突然发现——原来SQL根本不需要死记硬背,当它和具体的业务逻辑绑定在一起时,每个命令都变得合情合理。
今天,我们就用Express框架搭建一个完整的用户管理系统,在实现注册、登录、信息修改这些真实功能的过程中,让SQL语句自然融入你的肌肉记忆。你会惊讶地发现,当你在代码中写下第50遍SELECT * FROM users WHERE email = ?时,这些曾经枯燥的语法已经变成了条件反射。
1. 项目初始化与数据库准备
在开始编写业务代码前,我们需要先搭建好基础环境。这个阶段会涉及最基础的数据库操作,但别担心——我会解释每个SQL语句背后的实际意义。
首先创建项目目录并安装必要依赖:
mkdir user-management-system cd user-management-system npm init -y npm install express mysql2 body-parser接着在MySQL客户端中执行以下初始化脚本:
-- 创建数据库时指定UTF-8编码非常重要 CREATE DATABASE IF NOT EXISTS user_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 切换到新创建的数据库 USE user_system; -- 用户表结构设计 CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, email VARCHAR(100) NOT NULL UNIQUE, password_hash CHAR(60) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), INDEX idx_email (email) ) ENGINE=InnoDB;几个关键设计决策值得注意:
- 字符集选择:
utf8mb4支持完整的Unicode字符(包括emoji),比传统的utf8更全面 - 时间戳管理:
updated_at字段会在记录更新时自动刷新 - 索引优化:为
email字段添加索引加速查询 - 密码存储:使用固定长度的
CHAR(60)以适应bcrypt哈希值
提示:在真实项目中,建议将这类初始化脚本保存为
database/schema.sql文件,方便团队共享和版本控制。
2. 连接数据库与基础工具类
Node.js中操作数据库的第一步是建立连接池。与每次请求都创建新连接不同,连接池能显著提升性能:
// db.js const mysql = require('mysql2/promise'); const pool = mysql.createPool({ host: 'localhost', user: 'root', password: 'yourpassword', database: 'user_system', waitForConnections: true, connectionLimit: 10, queueLimit: 0 }); module.exports = { async query(sql, params) { const [rows] = await pool.query(sql, params); return rows; }, async getOne(sql, params) { const [rows] = await pool.query(sql, params); return rows[0]; } };这个工具类封装了两个常用方法:
query():执行查询并返回所有结果getOne():专门用于只需要单条记录的查询
接下来我们创建第一个路由测试连接:
// app.js const express = require('express'); const db = require('./db'); const app = express(); app.use(express.json()); app.get('/test-db', async (req, res) => { try { const users = await db.query('SELECT 1 + 1 AS solution'); res.json({ success: true, data: users }); } catch (err) { res.status(500).json({ error: err.message }); } }); app.listen(3000, () => console.log('Server running on port 3000'));用Postman访问http://localhost:3000/test-db,如果看到{"success":true,"data":[{"solution":2}]},说明数据库连接成功!
3. 用户注册功能的SQL实践
用户注册是我们系统的第一个核心功能,涉及INSERT语句的实际应用。我们先来看完整的路由处理:
// routes/auth.js const router = require('express').Router(); const bcrypt = require('bcrypt'); const db = require('../db'); router.post('/register', async (req, res) => { const { username, email, password } = req.body; // 验证输入 if (!username || !email || !password) { return res.status(400).json({ error: 'Missing required fields' }); } try { // 检查邮箱是否已注册 const existingUser = await db.getOne( 'SELECT id FROM users WHERE email = ?', [email] ); if (existingUser) { return res.status(409).json({ error: 'Email already in use' }); } // 密码哈希处理 const saltRounds = 10; const passwordHash = await bcrypt.hash(password, saltRounds); // 插入新用户 const result = await db.query( `INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)`, [username, email, passwordHash] ); res.status(201).json({ id: result.insertId, username, email }); } catch (err) { res.status(500).json({ error: err.message }); } }); module.exports = router;这个端点处理了完整的注册流程,其中几个SQL相关要点值得关注:
- 参数化查询:所有用户输入都通过
?占位符传递,避免SQL注入 - 错误处理:通过
try-catch捕获可能的数据库错误 - 结果利用:
insertId返回新创建记录的自增ID
常见错误场景处理:
| 错误类型 | SQL状态码 | 处理方式 |
|---|---|---|
| 重复邮箱 | ER_DUP_ENTRY | 返回409冲突状态 |
| 字段过长 | ER_DATA_TOO_LONG | 前端限制+后端验证 |
| 连接超时 | ETIMEDOUT | 重试机制+日志记录 |
4. 用户登录与会话管理
登录功能主要涉及SELECT查询和密码验证:
router.post('/login', async (req, res) => { const { email, password } = req.body; try { // 查找用户 const user = await db.getOne( `SELECT id, username, email, password_hash FROM users WHERE email = ?`, [email] ); if (!user) { return res.status(401).json({ error: 'Invalid credentials' }); } // 验证密码 const passwordMatch = await bcrypt.compare( password, user.password_hash ); if (!passwordMatch) { return res.status(401).json({ error: 'Invalid credentials' }); } // 创建会话(简化版) req.session.userId = user.id; res.json({ id: user.id, username: user.username, email: user.email }); } catch (err) { res.status(500).json({ error: err.message }); } });登录流程中的SQL技巧:
- 只查询必要字段:避免
SELECT *,明确列出所需字段 - 参数化查询:防止通过登录表单进行SQL注入
- 索引利用:
email字段上的索引加速查询
5. 用户信息管理
用户信息的增删改查(CRUD)是数据库操作的核心场景。我们先实现获取用户信息:
// routes/users.js router.get('/:id', async (req, res) => { try { const user = await db.getOne( `SELECT id, username, email, created_at, updated_at FROM users WHERE id = ?`, [req.params.id] ); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json(user); } catch (err) { res.status(500).json({ error: err.message }); } });更新用户信息展示了UPDATE语句的使用:
router.put('/:id', async (req, res) => { const { username } = req.body; try { const result = await db.query( `UPDATE users SET username = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [username, req.params.id] ); if (result.affectedRows === 0) { return res.status(404).json({ error: 'User not found' }); } res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } });关键点解析:
affectedRows:确认更新是否成功执行CURRENT_TIMESTAMP:自动更新updated_at字段- 参数化查询:防止注入攻击
删除用户则演示DELETE操作:
router.delete('/:id', async (req, res) => { try { const result = await db.query( 'DELETE FROM users WHERE id = ?', [req.params.id] ); if (result.affectedRows === 0) { return res.status(404).json({ error: 'User not found' }); } res.status(204).end(); } catch (err) { res.status(500).json({ error: err.message }); } });6. 高级查询技巧
真实项目中的查询往往比简单的CRUD复杂得多。让我们实现几个常见场景:
分页查询:
router.get('/', async (req, res) => { const { page = 1, limit = 10 } = req.query; const offset = (page - 1) * limit; try { const [users, total] = await Promise.all([ db.query( `SELECT id, username, email, created_at FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?`, [parseInt(limit), offset] ), db.getOne('SELECT COUNT(*) AS total FROM users') ]); res.json({ data: users, meta: { total: total.total, page: parseInt(page), limit: parseInt(limit), totalPages: Math.ceil(total.total / limit) } }); } catch (err) { res.status(500).json({ error: err.message }); } });条件查询:
router.get('/search', async (req, res) => { const { q, minCreated } = req.query; let query = 'SELECT id, username FROM users WHERE 1=1'; const params = []; if (q) { query += ' AND username LIKE ?'; params.push(`%${q}%`); } if (minCreated) { query += ' AND created_at >= ?'; params.push(new Date(minCreated)); } try { const users = await db.query(query, params); res.json(users); } catch (err) { res.status(500).json({ error: err.message }); } });7. 数据库迁移与结构变更
随着项目发展,数据库结构调整是不可避免的。以下是几种常见场景的处理方式:
添加新字段:
ALTER TABLE users ADD COLUMN avatar_url VARCHAR(255) NULL COMMENT '用户头像URL' AFTER email;修改字段类型:
ALTER TABLE users MODIFY COLUMN username VARCHAR(75) NOT NULL;添加外键约束(如果有关联表):
ALTER TABLE posts ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;创建索引优化查询:
CREATE INDEX idx_username ON users(username);在Node.js中执行迁移的推荐做法:
// migrations/20230501-add-avatar.js const db = require('../db'); async function run() { try { await db.query(`ALTER TABLE users...`); console.log('Migration completed'); } catch (err) { console.error('Migration failed:', err); } finally { process.exit(); } } run();8. 性能优化与最佳实践
经过基础功能实现后,让我们关注几个提升数据库操作效率的关键点:
连接池配置优化:
// 生产环境推荐配置 const pool = mysql.createPool({ connectionLimit: 25, // 根据服务器CPU核心数调整 queueLimit: 1000, // 防止内存溢出 waitForConnections: true, host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME, timezone: '+00:00', // 统一时区 charset: 'utf8mb4' });查询优化技巧:
- **避免SELECT ***:只查询需要的字段
- 合理使用索引:为常用查询条件创建索引
- 批量操作:使用
INSERT INTO ... VALUES (...), (...), ... - 事务处理:保证多个操作的原子性
事务处理示例:
async function transferFunds(senderId, receiverId, amount) { const conn = await pool.getConnection(); try { await conn.beginTransaction(); // 扣除发送方余额 await conn.query( 'UPDATE accounts SET balance = balance - ? WHERE user_id = ?', [amount, senderId] ); // 增加接收方余额 await conn.query( 'UPDATE accounts SET balance = balance + ? WHERE user_id = ?', [amount, receiverId] ); await conn.commit(); return true; } catch (err) { await conn.rollback(); throw err; } finally { conn.release(); } }监控与日志:
// 在db.js中添加查询日志 pool.on('connection', (conn) => { conn.on('query', (query) => { console.debug('Executing query:', query.sql); }); conn.on('error', (err) => { console.error('Connection error:', err); }); });9. 安全防护措施
数据库操作中的安全问题不容忽视,以下是必须实施的防护策略:
SQL注入防护:
- 始终使用参数化查询
- 避免直接拼接SQL字符串
- 使用ORM或查询构建器时仍需谨慎
敏感数据处理:
// 在查询中排除敏感字段 router.get('/:id', async (req, res) => { const user = await db.getOne( `SELECT id, username, created_at FROM users WHERE id = ?`, [req.params.id] ); // ... });密码重置安全:
-- 使用单独的token表存储重置令牌 CREATE TABLE password_reset_tokens ( token CHAR(64) PRIMARY KEY, user_id INT NOT NULL, expires_at DATETIME NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) );审计日志:
CREATE TABLE audit_logs ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT, action VARCHAR(50) NOT NULL, table_name VARCHAR(50), record_id INT, old_values JSON, new_values JSON, ip_address VARCHAR(45), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );在Node.js中实现审计日志中间件:
async function auditLog(req, res, next) { const originalSend = res.send; res.send = function(data) { if (req.user && req.method !== 'GET') { db.query( `INSERT INTO audit_logs (user_id, action, table_name, record_id, ip_address) VALUES (?, ?, ?, ?, ?)`, [req.user.id, req.method, req.params.table, req.params.id, req.ip] ).catch(console.error); } originalSend.apply(res, arguments); }; next(); }10. 测试与调试技巧
可靠的数据库操作需要完善的测试覆盖。以下是几种测试策略:
单元测试示例(使用Jest):
describe('User Repository', () => { let testUserId; beforeAll(async () => { // 创建测试用户 const result = await db.query( `INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)`, ['testuser', 'test@example.com', 'hashedpass'] ); testUserId = result.insertId; }); afterAll(async () => { // 清理测试数据 await db.query('DELETE FROM users WHERE id = ?', [testUserId]); }); test('should find user by email', async () => { const user = await db.getOne( 'SELECT id FROM users WHERE email = ?', ['test@example.com'] ); expect(user.id).toBe(testUserId); }); });查询性能分析:
-- 使用EXPLAIN分析查询执行计划 EXPLAIN SELECT * FROM users WHERE email = 'test@example.com'; -- 开启性能分析 SET profiling = 1; -- 执行查询 SELECT * FROM users WHERE username LIKE '%john%'; -- 查看分析结果 SHOW PROFILE;慢查询日志:
在MySQL配置文件中启用慢查询日志:
[mysqld] slow_query_log = 1 slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 1 log_queries_not_using_indexes = 1连接池监控:
// 定期检查连接池状态 setInterval(() => { console.log('Connection pool stats:', { total: pool.totalCount, active: pool.activeCount, idle: pool.idleCount, waiting: pool.waitingCount }); }, 60000);11. 项目结构与代码组织
随着项目规模扩大,良好的代码组织至关重要。推荐的结构如下:
src/ ├── config/ │ └── database.js # 数据库配置 ├── db/ │ ├── connection.js # 连接池管理 │ └── migrations/ # 数据库迁移脚本 ├── models/ │ └── User.js # 数据模型 ├── repositories/ │ └── UserRepository.js # 数据库访问层 ├── routes/ │ ├── auth.js # 认证路由 │ └── users.js # 用户路由 ├── services/ │ └── UserService.js # 业务逻辑 └── app.js # 应用入口数据访问层示例:
// repositories/UserRepository.js class UserRepository { async findByEmail(email) { return db.getOne( 'SELECT * FROM users WHERE email = ?', [email] ); } async create(userData) { const result = await db.query( `INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)`, [userData.username, userData.email, userData.passwordHash] ); return this.findById(result.insertId); } async findById(id) { return db.getOne( `SELECT id, username, email, created_at FROM users WHERE id = ?`, [id] ); } } module.exports = new UserRepository();服务层示例:
// services/UserService.js const userRepository = require('../repositories/UserRepository'); const bcrypt = require('bcrypt'); class UserService { async register(userData) { const existingUser = await userRepository.findByEmail(userData.email); if (existingUser) { throw new Error('Email already in use'); } const saltRounds = 10; const passwordHash = await bcrypt.hash(userData.password, saltRounds); return userRepository.create({ username: userData.username, email: userData.email, passwordHash }); } } module.exports = new UserService();这种分层架构的优势:
- 关注点分离:每层有明确职责
- 可测试性:各层可以独立测试
- 可维护性:数据库变更只需修改Repository层
- 复用性:相同Repository可以被多个Service使用
12. 生产环境部署建议
将Node.js应用与MySQL部署到生产环境时,需要考虑以下因素:
数据库连接配置:
// config/database.js module.exports = { host: process.env.DB_HOST || 'localhost', user: process.env.DB_USER || 'root', password: process.env.DB_PASS || '', database: process.env.DB_NAME || 'user_system', waitForConnections: true, connectionLimit: process.env.DB_POOL_SIZE || 10, queueLimit: 0, timezone: '+00:00', charset: 'utf8mb4' };连接健康检查:
// 定期检查连接是否存活 setInterval(async () => { try { await db.query('SELECT 1'); } catch (err) { console.error('Database connection lost:', err); // 触发重连或报警机制 } }, 30000);连接池事件监听:
pool.on('acquire', (connection) => { console.debug('Connection %d acquired', connection.threadId); }); pool.on('release', (connection) => { console.debug('Connection %d released', connection.threadId); }); pool.on('enqueue', () => { console.debug('Waiting for available connection slot'); });性能优化配置:
# MySQL生产配置建议 (my.cnf) [mysqld] innodb_buffer_pool_size = 4G # 总内存的50-70% innodb_log_file_size = 256M innodb_flush_log_at_trx_commit = 1 innodb_flush_method = O_DIRECT max_connections = 200 thread_cache_size = 10 table_open_cache = 4000备份策略:
# 每日数据库备份脚本 mysqldump -u root -p user_system | gzip > /backups/user_system_$(date +%Y%m%d).sql.gz # 保留最近7天备份 find /backups -name "*.sql.gz" -mtime +7 -delete13. 常见问题解决方案
在实际开发中,你可能会遇到以下典型问题及解决方法:
连接泄漏问题:
症状:应用运行一段时间后无法获取新连接 解决方案:
// 确保在所有代码路径中释放连接 async function getUser(id) { const conn = await pool.getConnection(); try { const [user] = await conn.query('SELECT * FROM users WHERE id = ?', [id]); return user; } finally { conn.release(); // 确保连接被释放 } }长事务问题:
症状:事务持有锁时间过长导致性能下降 解决方案:
- 设置事务超时
- 拆分大事务为多个小事务
- 添加应用层超时控制
async function processOrder() { const timeout = setTimeout(() => { throw new Error('Transaction timeout'); }, 5000); // 5秒超时 const conn = await pool.getConnection(); try { await conn.beginTransaction(); // ...业务逻辑... await conn.commit(); } catch (err) { await conn.rollback(); throw err; } finally { clearTimeout(timeout); conn.release(); } }字符集问题:
症状:存储或显示特殊字符时出现乱码 解决方案:
- 确保数据库、表和连接都使用
utf8mb4 - 检查HTTP请求/响应的Content-Type头
-- 修改已有表的字符集 ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;批量插入优化:
// 低效方式 for (const user of users) { await db.query('INSERT INTO users (...) VALUES (...)', [...]); } // 高效方式 const values = users.map(user => [ user.username, user.email, user.passwordHash ]); await db.query( 'INSERT INTO users (username, email, password_hash) VALUES ?', [values] );连接池耗尽处理:
// 监控并报警 pool.on('enqueue', () => { const waitCount = pool.waitingCount; if (waitCount > 10) { alertAdmin(`Connection pool queue growing: ${waitCount}`); } });14. 扩展知识:使用TypeScript增强安全性
为数据库操作添加类型安全可以显著减少运行时错误。以下是TypeScript集成示例:
定义用户类型:
// types/User.ts interface User { id: number; username: string; email: string; password_hash?: string; created_at: Date; updated_at: Date; } interface UserInput { username: string; email: string; password: string; }类型化查询方法:
// db.ts import { Pool } from 'mysql2/promise'; export async function query<T = any>( sql: string, params?: any[] ): Promise<T[]> { const [rows] = await pool.query(sql, params); return rows as T[]; } export async function getOne<T = any>( sql: string, params?: any[] ): Promise<T | null> { const [rows] = await pool.query(sql, params); return (rows as T[])[0] || null; }类型安全的Repository:
// repositories/UserRepository.ts import { User, UserInput } from '../types/User'; class UserRepository { async findByEmail(email: string): Promise<User | null> { return getOne<User>( 'SELECT * FROM users WHERE email = ?', [email] ); } async create(input: UserInput): Promise<User> { const passwordHash = await bcrypt.hash(input.password, 10); const result = await query( `INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)`, [input.username, input.email, passwordHash] ); return this.findById(result.insertId); } }TypeScript带来的优势:
- 字段类型自动检查
- 防止SQL查询与类型不匹配
- 更好的IDE自动完成
- 编译时捕获潜在错误
15. 性能监控与优化
持续监控数据库性能是生产环境的关键任务。以下是几种实用方法:
慢查询分析:
-- 查看慢查询日志中的Top 10 SELECT query_time, lock_time, rows_sent, rows_examined, sql_text FROM mysql.slow_log ORDER BY query_time DESC LIMIT 10;索引使用情况:
-- 检查未使用的索引 SELECT object_schema, object_name, index_name FROM performance_schema.table_io_waits_summary_by_index_usage WHERE index_name IS NOT NULL AND count_star = 0 ORDER BY object_schema, object_name;连接池监控仪表板:
// 暴露连接池指标给监控系统 app.get('/metrics', (req, res) => { res.json({ db_connections: { total: pool.totalCount, active: pool.activeCount, idle: pool.idleCount, waiting: pool.waitingCount } }); });查询缓存命中率:
SHOW STATUS LIKE 'Qcache%';性能优化 checklist:
- [ ] 为所有常用查询条件添加适当索引
- [ ] 避免在WHERE子句中对字段进行函数操作
- [ ] 使用EXPLAIN分析关键查询
- [ ] 定期优化表结构
- [ ] 监控连接池使用情况
- [ ] 设置合理的查询超时
16. 备选方案与高级主题
当项目规模扩大后,可能需要考虑更高级的数据库技术:
读写分离:
// 配置主从连接池 const masterPool = mysql.createPool({ /* 写配置 */ }); const replicaPool = mysql.createPool({ /* 读配置 */ }); // 根据查询类型选择连接池 function getPool(isWriteOperation) { return isWriteOperation ? masterPool : replicaPool; }分库分表策略:
-- 按用户ID分表 CREATE TABLE users_0 LIKE users; CREATE TABLE users_1 LIKE users; -- ...更多分表... -- 路由到特定分表 function getUserShard(id) { const shard = id % 10; return `users_${shard}`; }使用ORM的利弊:
优势:
- 更高级的抽象
- 自动类型转换
- 关系管理更简单
劣势:
- 性能开销
- 复杂查询可能更难优化
- 需要学习额外API
流行的Node.js ORM比较:
| ORM名称 | 特点 | 适合场景 |
|---|---|---|
| Sequelize | 功能全面,支持多种方言 | 中型应用,需要多种数据库支持 |
| TypeORM | 强类型支持,装饰器语法 | TypeScript项目,复杂领域模型 |
| Prisma | 类型安全,现代API | 新项目,开发体验优先 |
| Knex | 查询构建器,轻量级 | 需要灵活SQL控制的项目 |
原生SQL与ORM的选择建议:
- 简单项目:纯SQL或Knex
- 中型项目:Sequelize或TypeORM
- 大型项目:混合使用(核心功能用SQL,普通CRUD用ORM)
- TypeScript项目:优先考虑TypeORM或Prisma
17. 实战案例:电商用户模块
让我们通过一个电商平台的用户模块综合运用所学知识:
用户表结构设计:
CREATE TABLE shop_users ( user_id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, email VARCHAR(100) NOT NULL UNIQUE, phone VARCHAR(20), password_hash CHAR(60) NOT NULL, avatar_url VARCHAR(255), status ENUM('active', 'banned', 'deleted') DEFAULT 'active', last_login_at DATETIME, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (user_id), UNIQUE INDEX idx_email (email), INDEX idx_status (status) ) ENGINE=InnoDB;地址管理子系统:
CREATE TABLE user_addresses ( address_id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, recipient_name VARCHAR(100) NOT NULL, phone VARCHAR(20) NOT NULL, province VARCHAR(50) NOT NULL, city VARCHAR(50) NOT NULL, district VARCHAR(50) NOT NULL, detail_address VARCHAR(255) NOT NULL, is_default BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (address_id), INDEX idx_user (user_id), FOREIGN KEY (user_id) REFERENCES shop_users(user_id) ON DELETE CASCADE ) ENGINE=InnoDB;复合查询示例:
async function getUserWithAddresses(userId) { const [user, addresses] = await Promise.all([ db.getOne('SELECT * FROM shop_users WHERE user_id = ?', [userId]), db.query('SELECT * FROM user_addresses WHERE user_id = ?', [userId]) ]); if (!user) throw new Error('User not found'); return { ...user, addresses }; }事务处理示例:
async function setDefaultAddress(userId, addressId) { const conn = await pool.getConnection(); try { await conn.beginTransaction(); // 重置所有地址的默认状态 await conn.query( 'UPDATE user_addresses SET is_default = FALSE WHERE user_id = ?', [userId] ); // 设置新默认地址 const result = await conn.query( 'UPDATE user_addresses SET is_default = TRUE WHERE address_id = ? AND user_id = ?', [addressId, userId] ); if (result.affectedRows === 0) { throw new Error('Address not found or not belong to user'); } await conn.commit(); return true; } catch (err) { await conn.rollback(); throw err; } finally { conn.release(); } }18. 调试技巧与工具推荐
高效调试数据库问题是开发者必备技能。以下是我常用的工具链:
MySQL Workbench:
- 可视化执行计划分析
- 性能仪表板
- 数据建模工具
命令行调试技巧:
# 查看当前连接状态 mysqladmin -u root -p processlist # 实时监控查询 watch -n 1 "mysql -u root -p -e 'SHOW FULL PROCESSLIST'" # 性能分析 mysqlslap --user=root --password --concurrency=50 --iterations=10 --query="SELECT * FROM users"Node.js调试工具:
// 在代码中添加调试日志 db.query = async function(sql, params) { console.debug('Executing:', sql.replace(/\s+/g, ' '), params); const start = Date.now(); try { const [rows] = await pool.query(sql, params); console.debug(`Query took ${Date.now() - start}ms`); return rows; } catch (err) { console.error('Query failed:', err.message, {sql}); throw err; } };性能分析工具:
- Percona Toolkit:高级MySQL诊断工具集
- pt-query-digest:分析慢查询日志
- Prometheus + Grafana:可视化监控
- New Relic:全栈性能监控
常用诊断查询:
-- 查看当前锁情况 SELECT * FROM performance_schema.metadata_locks; -- 查看缓存命中率 SELECT SUM(rows_read) AS rows_read, SUM(rows_sent) AS rows_sent, SUM(rows_examined) AS rows_examined FROM performance_schema.events_statements_summary_by_digest; -- 查看表统计信息 SELECT table_name, engine, table_rows, avg_row_length, data_length FROM information_schema.tables WHERE table_schema = 'user_system';19. 安全加固措施
数据库安全需要多层次防护,以下是我在项目中实施的措施:
最小权限原则:
-- 创建专用应用用户 CREATE USER 'app_user'@'%' IDENTIFIED BY 'strong_password'; GRANT SELECT, INSERT, UPDATE, DELETE ON user_system.* TO 'app_user'@'%'; FLUSH PRIVILEGES;敏感数据加密:
-- 使用MySQL内置加密函数 UPDATE users SET ssn = AES_ENCRYPT('123-45-6789', 'encryption_key'); SELECT