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,
};
相关推荐
Logic1016 小时前
《数据库运维》 郭文明 实验5 数据库性能监视与优化实验核心操作与思路解析
运维·数据库·sql·mysql·计算机网络技术·形考作业·国家开放大学
你真的可爱呀6 小时前
4.前后端联调(Vue3+Vite + Express + MySQL)
mysql·node.js·vue·express
刺客xs6 小时前
Qt ---- Qt6.5.3 连接MySQL数据库
数据库·qt·mysql
37方寸6 小时前
MySQL系列4
mysql
weixin_462446236 小时前
【原创实践】Node.js 动态生成 SVG 项目规划纸模板 高仿 纸由我 PaperMe
node.js·生成纸张
光影少年7 小时前
postgrsql和mysql区别?
数据库·mysql·postgresql
云和数据.ChenGuang7 小时前
`post_max_size`、`max_execution_time`、`max_input_time` 是 **PHP 核心配置参数**
开发语言·mysql·php·zabbix·mariadb
PAQQ7 小时前
ubuntu22.04 搭建 Opencv & C++ 环境
前端·webpack·node.js
wsx_iot7 小时前
MySQL 的 MVCC(多版本并发控制)详解
数据库·mysql