当前位置: 首页 > news >正文

3.MySQL 数据库集成

核心目标

掌握 Node.js 连接 MySQL、基本 CRUD 操作,结合 Express 编写数据库接口。

MySQL 基础与环境准备

MySQL 入门

MySQL 是什么?
  • 是能按规则存数据、快速查数据、改数据,还能防止数据丢 / 乱.
  • 关系型:数据之间可建立关联。
  • 核心优势:数据结构化(表格形式)、支持复杂查询(比如 “查近 7 天买过牛奶的会员”)、数据安全(可备份、权限控制)。
MySQL 核心基本概念(库、表、字段):

MySQL 概念

生活化类比

通俗解释

数据库(库 / DB)

超市的 “总账本夹”

一个 MySQL 里可以建多个 “库”,每个库对应一个业务(比如 “超市数据” 库、“员工考勤” 库),互相隔离;比如 Express 项目里,一个库专门存 “博客系统” 的所有数据。

数据表(表 / Table)

总账本夹里的 “单本账本”

一个库里有多个表,每个表存一类数据(比如 “超市数据” 库里有:✅ 会员表(存会员 ID、姓名、电话);✅ 商品表(存商品 ID、名称、价格);✅ 订单表(存订单 ID、会员 ID、购买商品、金额)。

字段(列 / Column)

账本里的 “列标题”

每个表由多个字段组成,字段是数据的 “属性”;比如 “会员表” 的字段:ID、姓名、电话、注册时间(每一列就是一个字段)。

行(记录 / Row)

账本里的 “每一行记录”

字段是 “列标题”,行就是具体的数据;比如会员表的一行:1、张三、138xxxx1234、2025-01-01

→ 这是一个会员的完整信息。

主键(Primary Key)

账本里的 “唯一编号”

为了区分每一行数据,给表指定一个 “唯一标识字段”(比如会员表的 “ID”),保证每行都不一样(不会有两个 ID=1 的张三)。

SQL 语句:操作 MySQL 的 “指令”

SQL(结构化查询语言)是和 MySQL 对话的 “语言”,你输入指令,MySQL 执行对应的操作(增 / 删 / 改 / 查数据)。

1. 查(最常用):SELECT → 找数据
SELECT * FROM 会员表 WHERE 姓名 = '张三';
2. 增:INSERT → 加数据
INSERT INTO 会员表 (ID, 姓名, 电话) VALUES (3, '王五', '137xxxx9999');
3. 改:UPDATE → 改数据
UPDATE 会员表 SET 电话 = '138xxxx4321' WHERE ID = 1;
4. 删:DELETE → 删数据
DELETE FROM 会员表 WHERE ID = 3;

建库:CREATE DATABASE 超市数据;

建表:CREATE TABLE 会员表 (ID INT, 姓名 VARCHAR(20), 电话 VARCHAR(11));

和 Express 结合的核心逻辑

整体流程:环境准备 → 创建 MySQL 库/表 → 搭建 Express 项目 → 配置 MySQL 连接 → 编写 CRUD 接口 → 测试接口

配置 MySQL 连接(核心桥梁)

将 Express 项目和 MySQL 打通,核心是创建「连接池」(复用连接,性能更高)。

1 新建数据库连接模块(db/index.js)+封装事务通用逻辑。

npm i mysql2 用 Promise 版本(适配 async/await)

npm i dotenv 环境变量管理

// 引入依赖 const mysql = require('mysql2/promise'); const dotenv = require('dotenv'); // 加载环境变量 dotenv.config({ path: './config/.env' }); // 数据库连接配置(从环境变量读取) const dbConfig = { host: process.env.DB_HOST, port: process.env.DB_PORT, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, charset: process.env.DB_CHARSET, connectionLimit: process.env.DB_CONNECTION_LIMIT, // 连接池最大连接数 waitForConnections: true, // 连接池满时等待 queueLimit: 0 // 等待队列无上限 }; // 创建连接池(核心:复用连接,提升性能) const pool = mysql.createPool(dbConfig); // 封装通用查询方法(所有 SQL 操作都通过这个方法,统一处理) // execute连接池中处理sql的方法 async function query(sql, params = []) { try { const [rows] = await pool.execute(sql, params); return { success: true, data: rows }; } catch (err) { console.error('SQL 执行失败:', sql, params, err.message); return { success: false, error: err.message }; } } // 通用事务逻辑方法 async function executeTransaction(tasks) { let connection; // 声明独立连接变量(原因:事务必须用独立连接,避免多请求共享连接导致事务混乱) try { // 从连接池获取独立连接(原因:连接池默认是共享连接,事务需要独占连接) connection = await pool.getConnection(); // 开启事务(原因:关闭MySQL的自动提交模式,后续SQL需手动提交) await connection.beginTransaction(); // 批量执行事务任务(原因:按顺序执行所有跨表操作,保证逻辑顺序) const results = []; // 存储每个SQL的执行结果(便于后续排查) for (const task of tasks) { // 执行单个SQL(使用?占位符,原因:防止SQL注入,这是安全必备) const [rows] = await connection.execute(task.sql, task.params); results.push(rows); // 记录结果 } // 提交事务(原因:所有SQL执行成功,确认修改生效) await connection.commit(); // 返回成功结果(results包含每个SQL的执行结果,便于业务层判断) return { success: true, data: results }; } catch (err) { // 事务回滚(原因:任意SQL执行失败,撤销所有已执行的修改,保证原子性) if (connection) { // 先判断连接是否获取成功(避免空指针) await connection.rollback(); } // 返回失败结果(包含错误信息,便于业务层返回友好提示) return { success: false, error: err.message }; } finally { // 释放连接(原因:无论成功/失败,必须释放连接到连接池,否则连接池会被耗尽) if (connection) { connection.release(); } } } // 导出连接池和通用查询方法 module.exports = { pool, query, executeTransaction };

其中:const [rows] = await pool.execute(sql, params);中的rows是mysql2的特定返回结果载体pool.execute()的返回值是一个数组。


补充:事务通俗来说就是,我们在方法中使用sql语句来操作表1时(例如删除用户),表2 和表3中的某条数据受影响了(例如他们都是基于用户的数据),就需要在操作表1之前先把表2和表三的相关数据进行操作(例如删除),删除成功,大家都删除,只要有一个删除失败,就回滚恢复原样都不删除。这样一个过程就叫事务。

先理解事务基础逻辑,再去进行使用封装。


不同的SQL 类型下,rows 的具体含义不同:

1. 读操作(SELECT):rows 是「查询结果数组」

// 执行查询 SQL const sql = 'SELECT id, name FROM user WHERE age > ?'; const params = [18]; const [rows] = await pool.execute(sql, params); // rows 是数组,每个元素是一行数据(对象) console.log(rows); // 输出示例:[{id: 1, name: '张三'}, {id: 2, name: '李四'}] console.log(rows.length); // 2(查询到2行) // 你的返回值:{success: true, data: [{id:1, name:'张三'}, ...]} return { success: true, data: rows };

2. 写操作(INSERT):rows 是「执行结果对象」

# 核心包含 affectedRows(插入的行数)和 insertId(自增主键 ID),无实际数据行。 // 执行插入 SQL const sql = 'INSERT INTO user(name, age) VALUES(?, ?)'; const params = ['赵六', 20]; const [rows] = await pool.execute(sql, params); // rows 是执行结果对象 console.log(rows); // 输出示例:{ // affectedRows: 1, // 插入了1行 // insertId: 102, // 自增ID为102 // fieldCount: 0, // serverStatus: 2, // warningCount: 0, // ... // } // 你的返回值:{success: true, data: {affectedRows:1, insertId:102}} return { success: true, data: rows };

3. 写操作(UPDATE):rows 是「执行结果对象」

# 核心是 affectedRows(实际更新的行数)—— 即使 SQL 执行成功,若没有匹配的记录 / 字段值未变化,affectedRows 可能为 0。 // 执行更新 SQL const sql = 'UPDATE user SET age = ? WHERE id = ?'; const params = [21, 102]; const [rows] = await pool.execute(sql, params); // rows 是执行结果对象 console.log(rows); // 输出示例:{ // affectedRows: 1, // 更新了1行 // changedRows: 1, // 实际修改的字段行数(区别于affectedRows) // warningCount: 0, // ... // } // 你的返回值:{success: true, data: {affectedRows:1, changedRows:1}} return { success: true, data: rows };

4. 写操作(DELETE):rows 是「执行结果对象」

// 执行删除 SQL const sql = 'DELETE FROM user WHERE id = ?'; const params = [102]; const [rows] = await pool.execute(sql, params); // rows 是执行结果对象 console.log(rows); // 输出示例:{affectedRows: 1, warningCount: 0, ...} // 你的返回值:{success: true, data: {affectedRows:1}} return { success: true, data: rows };
2 业务封装层

调用事务

const { executeTransaction } = require('../config/db'); class UserDb { //删除 static async deleteUserWithRelations(userId) { // 前置校验1:判断用户是否存在(原因:先校验非事务操作,减少事务执行时间,避免长事务锁表) const [userExist] = await pool.execute('SELECT id FROM user WHERE id = ?', [userId]); if (userExist.length === 0) { return { success: false, error: '用户不存在,无需删除' }; } // 前置校验2:格式化参数(原因:确保userId是数字,避免SQL执行异常) const targetUserId = Number(userId); if (isNaN(targetUserId)) { return { success: false, error: '用户ID格式错误' }; } // 定义事务任务数组(核心:按“先删关联数据,后删主数据”的顺序,原因:避免外键约束报错) const transactionTasks = [ // 任务1:删除该用户的所有物品(原因:先删子表,再删主表,避免外键约束阻止删除) { sql: 'DELETE FROM goods WHERE user_id = ?', // 精准删除该用户的物品 params: [targetUserId] // 占位符参数(防注入) }, // 任务2:删除该用户的所有朋友(原因:同理,先删关联数据) { sql: 'DELETE FROM frieds WHERE user_id = ?', params: [targetUserId] }, // 任务3:删除用户本身(原因:最后删主表,确保关联数据已清理) { sql: 'DELETE FROM user WHERE id = ?', params: [targetUserId] } ]; // 调用通用事务方法(原因:复用事务逻辑,避免重复写try/catch/回滚) const transactionResult = await executeTransaction(transactionTasks); // 处理事务结果(原因:给业务层返回清晰的执行状态) if (transactionResult.success) { // 解析每个任务的执行结果(便于前端/日志展示) const [goodsDeleteRes, relativeDeleteRes, userDeleteRes] = transactionResult.data; return { success: true, msg: '用户及关联数据删除成功', data: { deletedGoodsCount: goodsDeleteRes.affectedRows, // 删除物品数量 deletedRelativeCount: relativeDeleteRes.affectedRows, // 删除朋友数量 deletedUserCount: userDeleteRes.affectedRows // 删除用户数量(正常是1) } }; } else { // 事务失败,返回错误信息(原因:便于排查问题,如外键冲突、SQL错误) return { success: false, error: transactionResult.error }; } } } // 导出类(原因:模块化封装,路由层只需调用方法,无需关心SQL细节) module.exports = UserDb;

class(类)+static(静态方法:挂载到类)的核心目的是:在 “模块化封装” 的基础上,兼顾代码的可读性、可维护性、扩展性,同时避免无意义的实例化开销

设计点

解决的问题

class

1. 按业务模块聚类方法,形成清晰的业务边界;2. 支持继承 / 静态属性,适配复杂扩展;3. 语义化更强,符合 “模块封装” 的认知;

static

1. 避免无意义的实例化,简化调用语法;2. 无实例状态,避免多请求下的状态污染;3. 符合 “工具类” 的使用习惯(如 Math、Date)

3 路由层调用
const express = require('express'); const router = express.Router(); const UserDb = require('../db/userDb');//业务封装逻辑地址 const { success, error } = require('../utils/response'); // 统一响应封装:成功失败的逻辑封装 router.delete('/:id', async (req, res) => { try { //获取URL中的用户ID(原因:RESTful风格,路径参数传递资源ID) const userId = parseInt(req.params.id); //基础参数校验(原因:提前过滤无效参数,减少数据库请求) if (isNaN(userId) || userId <= 0) { return error(res, '用户ID必须为正整数', 400); } //调用业务层的事务方法(原因:路由层只做请求分发,不写业务逻辑) const result = await UserDb.deleteUserWithRelations(userId); //处理返回结果(统一响应格式,原因:前端无需适配不同的返回结构) if (result.success) { return success(res, result.data, result.msg); } // 区分业务错误和系统错误(原因:返回不同的HTTP状态码,便于前端处理) if (result.error.includes('用户不存在')) { return error(res, result.error, 404); // 404:资源不存在 } else { return error(res, `删除失败:${result.error}`, 500); // 500:服务器错误 } } catch (err) { // 全局异常捕获(原因:防止未处理的异常导致服务崩溃) console.error('删除用户接口异常:', err.stack); return error(res, '服务器内部错误', 500, err.message); } }); module.exports = router;
4 自动路由挂载工具

前面的核心逻辑 如果有多个就会有多个路由文件。每个路由文件独立封装对应模块的接口,统一导出express.Router()实例。这时候可以封装一个方法自动挂载。
使用工具:fs(nodejs的内置模块,封装了操作系统文件的能力,直接使用,无需安装)

const fs = require('fs'); const path = require('path'); const express = require('express'); function loadRouters(app) { // 获取routes目录的绝对路径(兼容不同系统) const routesDir = path.resolve(__dirname, '../routes'); // 读取routes目录下所有文件 fs.readdirSync(routesDir).forEach((file) => { // 过滤非.js文件、隐藏文件(如 .DS_Store) if (!file.endsWith('.js') || file.startsWith('.')) return; // 提取模块名(如 goodsOwnedRouter.js → goods-owned) // 规则:去掉Router.js后缀,驼峰转连字符(符合RESTful路径规范) const moduleName = file.replace('Router.js', '') .replace(/([A-Z])/g, '-$1') // 驼峰转连字符(GoodsOwned → goods-owned) .toLowerCase() // 转小写 .replace(/^-/, ''); // 去掉开头的- // 拼接路由文件路径,引入Router实例 const routerPath = path.join(routesDir, file); const router = require(routerPath); // 自动挂载路由:前缀 /api/模块名(如 /api/user、/api/goods-owned) const apiPrefix = `/api/${moduleName}`; app.use(apiPrefix, router); }); } module.exports = { loadRouters };
5 入口文件配置 app.js

统一整合中间件、路由自动挂载、数据库连接、端口监听,是项目的 “总开关”。

const express = require('express'); const dotenv = require('dotenv'); const { testDbConnection } = require('./config/db'); const { loadRouters } = require('./utils/routerLoader'); // 路由自动挂载 const { success, error } = require('./utils/response'); // 统一响应 // 加载环境变量(端口、数据库信息) dotenv.config({ path: `.env.${process.env.NODE_ENV || "development"}` }); // 创建Express实例 const app = express(); const PORT = process.env.PORT || 3000; // 优先读环境变量,默认3000 // 通用中间件配置(必须放在路由挂载前!) app.use(express.json()); // 解析JSON请求体 app.use(express.urlencoded({ extended: true })); // 解析表单请求体 // 全局错误处理中间件(捕获所有路由的异常) app.use((err, req, res, next) => { console.error('全局错误:', err.stack); error(res, '服务器内部错误', 500, err.message); }); // 启动流程(先连数据库,再挂载路由,最后监听端口) async function startServer() { try { //测试数据库连接(失败则退出) await testDbConnection(); // 自动挂载所有路由(核心!无需手动app.use) loadRouters(app); //监听端口(必须!否则接口无法访问) app.listen(PORT, () => { console.log("监听端口"); }); } catch (err) { console.error('❌ 服务启动失败:', err.message); process.exit(1); // 退出进程 } } // 执行启动 startServer();
6 环境变量配置

.env.development

# 开发环境配置 DB_HOST=localhost DB_USER=root DB_PASSWORD=root DB_DATABASE=text01 DB_PORT=3306 PORT= 3000 # 额外开发配置:比如调试模式、日志级别 NODE_ENV=development LOG_LEVEL=debug

.env.production

# 生产环境配置 # 生产数据库地址(云服务器/内网IP) DB_HOST=10.0.0.5 # 生产专用账号(非root) DB_USER=prod_express # 高强度密码 DB_PASSWORD=Pro@Db123456! # 生产正式库 DB_DATABASE=prod_ecommerce DB_PORT=3306 # 生产端口(HTTP默认80) PORT=80 # 生产配置:关闭调试、日志级别为error NODE_ENV=production LOG_LEVEL=error
7 统一响应封装response.js
const success = (res, data = null, msg = "操作成功", code = 200) => { // 确保status是合法的HTTP状态码(避免传入非标准码导致报错) const validStatus = [200, 201, 204].includes(code) ? code : 200; res.status(validStatus).json({ code, // 业务码(可自定义,如20000) msg, data, success: true, timestamp: new Date().getTime(), // 新增:响应时间戳(便于排查问题) }); }; const error = (res, msg = "操作失败", code = 500, error = null) => { // 区分HTTP状态码和业务码:HTTP状态码只能是标准值(400/404/500等) let httpStatus = code; // 校验HTTP状态码合法性,非法则默认500 if (![400, 401, 403, 404, 409, 500].includes(httpStatus)) { httpStatus = 500; } res.status(httpStatus).json({ code, // 业务码(可自定义,如50000) msg, // 生产环境隐藏错误详情,且错误信息转成字符串(避免循环引用) error: process.env.NODE_ENV === "development" ? (error instanceof Error ? error.stack : String(error)) : null, success: false, timestamp: new Date().getTime(), // 新增:响应时间戳 }); }; module.exports = { success, error, };
http://www.jsqmd.com/news/82308/

相关文章:

  • 23、利用SSH密钥、NFS挂载源目录和跨平台Python管理系统
  • 【AUTOSAR通信】Com简介(7)——MDT
  • LMCache深度解析:如何让大语言模型推理快10倍
  • 耗尽型mos管和增强型mos管的区别
  • 【给学生】# [特殊字符] 错题的正确打开方式
  • 告别仓库臃肿!Git LFS超详细安装实战手册
  • 24、操作系统管理与云计算技术中的Python应用
  • (Company Code)后,系统会自动创建一个与之同名且一一对应的“主导分类账”(Leading Ledger),默认标识符就是 0L。所有模块(FI、MM、SD、PP等)的业务交易在生成会计凭证
  • 1Panel面板OpenResty安装失败的终极解决方案指南
  • Visibility.js 终极指南:掌握页面可见性优化
  • 中泰证券|如何选择一款既满足降本增效,又能承载大几十套系统集群平台化管理的国产数据库?
  • 如何快速实现自动化任务:面向普通用户的青龙面板终极指南
  • Cropper.js完全指南:打造专业级前端图像裁剪功能
  • 2025年上海小红书代运营服务商最新排名揭晓,短视频运营公司/小红书代运营/企业号代运营/短视频获客小红书代运营服务商推荐排行榜单 - 品牌推荐师
  • DataGear数据可视化分析平台:5分钟快速上手终极指南
  • 有没有能对接金蝶的 MES 系统推荐?黑湖小工单:中小制造数据协同最优解
  • 25、技术探索:从数据查询到包管理的实用指南
  • Librum电子书阅读器完整使用教程:打造个人数字图书馆
  • 探索3D分形艺术:Mandelbulber 2如何用数学创造视觉奇迹?
  • 终极指南:5个步骤零成本搭建专业企业官网
  • Maestro Studio终极指南:零代码打造专业级移动测试
  • 哪款系统在生成各种生产报表(如日报、月报)方面比较强大?——制造企业全流程决策指南
  • 力扣打卡每日一题————零钱兑换
  • M+字体完全指南:免费开源的多语言字体解决方案
  • 解锁移动端语音合成新境界:5步构建轻量级TTS系统
  • 26、Python包管理与Egg创建全攻略
  • 如何用BetterTouchTool打造个性化Touch Bar体验:从预设到自定义
  • 2025年12月伺服压机品牌推荐排行榜:性能对比与行业应用深度评测 - 十大品牌推荐
  • 2025年10年Vue方向前端复习技术要点(2)
  • “医疗专业应用+分布式数据底座”:平凯数据库与金唐软件全链路赋能医疗国产化与数字化转型