八:数据库入门——MySQL 与增删改查

一、回顾与本篇目标

上一篇我们用 JSON 文件存储用户数据,实现了完整的增删改查。这是一个重要的里程碑——你的后端服务终于有了“记忆”,重启之后数据还在。

但文件存储有它的天花板:数据量大了读写会慢、想按条件查询只能遍历整个数组、多个请求同时写文件可能出问题。这些问题不是说文件存储不好,而是“用文件做数据库”这件事本来就不是文件的强项。文件的强项是存配置、存日志、存静态内容。数据的增删改查,应该交给专业的工具——数据库

本篇的目标:

  1. 理解数据库是什么、为什么需要它
  2. 安装 MySQL,创建数据库和表
  3. 学会最核心的 SQL 语句:增删改查
  4. 用 Node.js 连接 MySQL,把上一篇的文件存储替换成真正的数据库

二、数据库到底是什么

如果你用过 Excel,你就已经理解了数据库 80% 的概念。

一个 Excel 文件就是一个数据库。文件里的每一张工作表(Sheet)就是一张。表的第一行是列名(字段),下面的每一行是一条记录

id name email
1 张三 zhangsan@example.com
2 李四 lisi@example.com
3 王五 wangwu@example.com

翻译成数据库术语:

  • 这个 Excel 文件 → 数据库
  • 这张 Sheet →
  • 第一行的 id、name、email → 字段(列)
  • 下面的每一行 → 记录(行)

数据库和 Excel 的区别在于:数据库不是给人手动翻看的,而是给程序高速读写的。它能同时处理成千上万个请求,能对百万级的数据做亚秒级查询,能保证数据不会因为断电而损坏。这些 Excel 做不到。

MySQL 是世界上最流行的关系型数据库之一。“关系型”的意思就是数据以表格的形式组织,表和表之间可以通过字段关联(后面会学到)。它免费、开源、稳定,是学习数据库的最佳起点。

三、安装 MySQL

Windows 安装

  1. 打开浏览器,访问 https://dev.mysql.com/downloads/mysql/
  2. 找到 MySQL Community Server,选择 Windows 平台,下载 MSI Installer 版本。
  3. 双击下载好的安装包,选择 Custom 安装类型。
  4. 在安装组件列表中,确保勾选了 MySQL Server
  5. 一路点 Next,设置 root 用户的密码时,设一个你记得住的密码(比如 123456,学习阶段简单点没关系)。
  6. 完成安装。

Mac 安装

Mac 上推荐用 Homebrew 安装。如果你还没装 Homebrew,先去 https://brew.sh 按照说明安装。

然后打开终端,执行:

brew install mysql

安装完成后,启动 MySQL 服务:

brew services start mysql

设置 root 密码(初次安装时 root 没有密码):

mysql -u root

进入 MySQL 命令行后,执行:

ALTER USER 'root'@'localhost' IDENTIFIED BY '123456';
FLUSH PRIVILEGES;
EXIT;

验证安装是否成功

打开终端(Windows 可以在开始菜单找到“MySQL Command Line Client”),输入:

mysql -u root -p

提示输入密码时,输入你刚才设置的密码。如果出现 mysql> 这样的提示符,说明安装成功。输入 EXIT; 退出。

四、创建数据库和表

在 MySQL 中,数据是按照“数据库 → 表 → 记录”的层级组织的。我们一步一步来。

登录 MySQL

mysql -u root -p

输入密码,进入 MySQL 命令行。

创建数据库

CREATE DATABASE myapp;

注意 SQL 语句以分号结尾,这是语法规定。如果忘了打分号,MySQL 会以为你还没输完,等你继续输入。

myapp 是数据库的名字,你可以起任何名字。命名习惯:小写字母,单词之间用下划线连接。

查看有哪些数据库

SHOW DATABASES;

你会看到一个列表,包括刚创建的 myapp 以及 MySQL 自带的几个系统数据库。

选择要使用的数据库

USE myapp;

这条命令告诉 MySQL:“我接下来的操作都是针对 myapp 这个数据库的。”

创建表

我们来创建一张用户表,存储 ID、姓名、邮箱:

CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  email VARCHAR(200) NOT NULL UNIQUE
);

逐行解释:

  • CREATE TABLE users:创建一张名为 users 的表。
  • id INT AUTO_INCREMENT PRIMARY KEY
    • INT:整数类型。
    • AUTO_INCREMENT:自动增长。插入新记录时不用手动指定 ID,MySQL 会自动分配一个递增的数字(1、2、3……)。
    • PRIMARY KEY:主键。唯一标识每一条记录,不能重复、不能为空。就像身份证号,一个表里每条记录的 ID 都是唯一的。
  • name VARCHAR(100) NOT NULL
    • VARCHAR(100):可变长度的字符串,最多 100 个字符。
    • NOT NULL:不允许为空。插入数据时这个字段必须有值。
  • email VARCHAR(200) NOT NULL UNIQUE
    • UNIQUE:值必须唯一。两个人的邮箱不能重复。

查看表的结构

DESCRIBE users;

你会看到表的字段名、类型、是否允许为空、键类型等信息。

五、SQL 语句入门:增删改查

数据库的核心操作就四个,简称为 CRUD

  • Create(创建)→ INSERT
  • Read(读取)→ SELECT
  • Update(更新)→ UPDATE
  • Delete(删除)→ DELETE

所有操作都通过 SQL 语句来完成。SQL 是一门专门用来操作数据库的语言,语法接近英语自然语言,读起来比较直观。

1. 插入数据:INSERT

INSERT INTO users (name, email) VALUES ('张三', 'zhangsan@example.com');
INSERT INTO users (name, email) VALUES ('李四', 'lisi@example.com');
INSERT INTO users (name, email) VALUES ('王五', 'wangwu@example.com');

解释

  • INSERT INTO users:往 users 表里插入数据。
  • (name, email):指定要插入哪几个字段。注意我们没有写 id,因为它是 AUTO_INCREMENT,会自动生成。
  • VALUES ('张三', 'zhangsan@example.com'):对应字段的值。字符串用单引号包裹。

执行成功后,MySQL 会返回 Query OK, 1 row affected

2. 查询数据:SELECT

-- 查询所有用户的所有字段
SELECT * FROM users;

-- 查询所有用户的姓名和邮箱
SELECT name, email FROM users;

-- 查询 ID 为 1 的用户
SELECT * FROM users WHERE id = 1;

-- 查询名字叫张三的用户
SELECT * FROM users WHERE name = '张三';

解释

  • SELECT ** 表示所有字段。
  • FROM users:从 users 这张表里查。
  • WHERE:过滤条件。只有满足条件的记录才会被返回。WHERE 后面跟的是条件表达式,和 JavaScript 的 if 判断类似。
  • 两条短横线 -- 是 SQL 中的注释。

3. 更新数据:UPDATE

-- 把 ID 为 1 的用户名字改成“张三丰”
UPDATE users SET name = '张三丰' WHERE id = 1;

-- 把所有用户的邮箱域名都改成 @newcompany.com(危险操作,慎用!)
-- UPDATE users SET email = REPLACE(email, '@example.com', '@newcompany.com');

关键提醒UPDATE 如果不加 WHERE,会更新所有记录!想象一下把全公司所有人的名字都改成了同一个人——这就是忘了写 WHERE 的后果。在执行 UPDATE 或 DELETE 之前,一定要确认 WHERE 条件写对了。

4. 删除数据:DELETE

-- 删除 ID 为 3 的用户
DELETE FROM users WHERE id = 3;

同样要小心:不加 WHEREDELETE FROM users; 会删掉整张表的所有数据。

六、用 Node.js 连接 MySQL

上一节我们在 MySQL 命令行里手敲 SQL,那是管理员的操作方式。后端程序需要通过代码来执行 SQL。Node.js 连接 MySQL 需要一个第三方模块——mysql2

安装 mysql2

在项目文件夹里执行:

npm install mysql2

创建数据库连接模块 db.js

// db.js —— 数据库连接模块

const mysql = require('mysql2');

// 创建数据库连接池
const pool = mysql.createPool({
  host: 'localhost',      // 数据库的地址,本地就是 localhost
  user: 'root',           // 数据库用户名
  password: '123456',     // 数据库密码(你安装时设置的)
  database: 'myapp',      // 要连接的数据库名
  waitForConnections: true,
  connectionLimit: 10,    // 最多同时 10 个连接
});

// 把连接池导出为 Promise 形式,方便用 async/await
module.exports = pool.promise();

解释:

  • 连接池:预先创建好几个数据库连接,请求来的时候直接从池子里取,用完还回去。避免了每次请求都重新连接数据库的开销。
  • pool.promise():把回调形式的 API 转成 Promise 形式,这样我们就可以用 await 来调用,代码更清晰。

测试连接

新建 test-db.js

// test-db.js —— 测试数据库连接

const db = require('./db.js');

async function test() {
  try {
    // 执行一条简单的查询
    let result = await db.query('SELECT 1 + 1 AS sum');
    console.log('数据库连接成功!1 + 1 =', result[0][0].sum);
  } catch (error) {
    console.error('数据库连接失败:', error.message);
  }
}

test();

执行 node test-db.js,如果输出“数据库连接成功!1 + 1 = 2”,说明一切正常。

注意 result[0] 的返回值结构db.query() 返回一个数组,result[0] 是查询结果的行数组,result[1] 是字段信息。我们通常只需要 result[0]

七、用数据库重写用户管理 API

现在我们把上一篇用文件存储的用户管理,改成用 MySQL 数据库。对比一下两种实现方式的差异。

创建 app.js

// app.js —— 用 MySQL 数据库的用户管理 Express 服务

const express = require('express');
const db = require('./db.js');
const app = express();

app.use(express.json());

// ========== 获取所有用户 ==========
app.get('/api/users', async function (request, response) {
  try {
    let result = await db.query('SELECT * FROM users');
    response.json(result[0]);
  } catch (error) {
    console.error('查询用户失败:', error.message);
    response.status(500).json({ error: '服务器内部错误' });
  }
});

// ========== 根据 ID 获取单个用户 ==========
app.get('/api/users/:id', async function (request, response) {
  try {
    let id = Number(request.params.id);
    let result = await db.query('SELECT * FROM users WHERE id = ?', [id]);

    if (result[0].length === 0) {
      return response.status(404).json({ error: '用户不存在' });
    }

    response.json(result[0][0]);  // 返回第一条(也是唯一一条)记录
  } catch (error) {
    console.error('查询用户失败:', error.message);
    response.status(500).json({ error: '服务器内部错误' });
  }
});

// ========== 添加新用户 ==========
app.post('/api/users', async function (request, response) {
  try {
    let { name, email } = request.body;

    if (!name || !email) {
      return response.status(400).json({ error: '缺少必填字段:name 和 email' });
    }

    // 用 ? 占位符防止 SQL 注入
    let result = await db.query(
      'INSERT INTO users (name, email) VALUES (?, ?)',

[name, email]

); // result[0].insertId 是自动生成的 ID response.status(201).json({ id: result[0].insertId, name: name, email: email }); } catch (error) { // 处理邮箱重复的错误 if (error.code === ‘ER_DUP_ENTRY’) { return response.status(409).json({ error: ‘该邮箱已被注册’ }); } console.error(‘添加用户失败:’, error.message); response.status(500).json({ error: ‘服务器内部错误’ }); } }); // ========== 更新用户 ========== app.put(‘/api/users/:id’, async function (request, response) { try { let id = Number(request.params.id); let { name, email } = request.body; // 先检查用户是否存在 let checkResult = await db.query(‘SELECT * FROM users WHERE id = ?’, [id]); if (checkResult[0].length === 0) { return response.status(404).json({ error: ‘用户不存在’ }); } // 构建更新 SQL let updateFields = []; let updateValues = []; if (name !== undefined) { updateFields.push(‘name = ?’); updateValues.push(name); } if (email !== undefined) { updateFields.push(’email = ?’); updateValues.push(email); } if (updateFields.length === 0) { return response.status(400).json({ error: ‘没有提供要更新的字段’ }); } updateValues.push(id); await db.query( ‘UPDATE users SET ‘ + updateFields.join(‘, ‘) + ‘ WHERE id = ?’, updateValues ); // 返回更新后的用户 let updatedResult = await db.query(‘SELECT * FROM users WHERE id = ?’, [id]); response.json(updatedResult[0][0]); } catch (error) { if (error.code === ‘ER_DUP_ENTRY’) { return response.status(409).json({ error: ‘该邮箱已被其他用户使用’ }); } console.error(‘更新用户失败:’, error.message); response.status(500).json({ error: ‘服务器内部错误’ }); } }); // ========== 删除用户 ========== app.delete(‘/api/users/:id’, async function (request, response) { try { let id = Number(request.params.id); let result = await db.query(‘DELETE FROM users WHERE id = ?’, [id]); if (result[0].affectedRows === 0) { return response.status(404).json({ error: ‘用户不存在’ }); } response.json({ message: ‘用户已删除’ }); } catch (error) { console.error(‘删除用户失败:’, error.message); response.status(500).json({ error: ‘服务器内部错误’ }); } }); // ========== 启动服务 ========== app.listen(3000, function () { console.log(‘服务已启动,访问 http://localhost:3000’); console.log(‘数据库:MySQL → myapp → users’); });

核心变化点(和文件存储对比):

  • 不再手动读写文件:所有数据操作变成 SQL 语句。
  • ID 自动生成:数据库的 AUTO_INCREMENT 自动分配 ID,不需要手动计算最大 ID + 1。
  • 条件查询由数据库完成WHERE id = ? 让数据库帮我们筛选,不需要读全表再手动遍历。
  • 并发安全:数据库自己处理并发读写冲突,不会出现文件写到一半被另一个请求打断的问题。
  • 错误处理更精细:可以捕获邮箱重复(ER_DUP_ENTRY)等具体的数据库错误。

八、SQL 注入与参数化查询

上面代码里有一个非常重要的写法,需要专门解释。你可能注意到 SQL 语句里用了问号 ?,然后用户输入的值是通过数组传递的,而不是直接拼接到 SQL 字符串里。

错误写法(永远不要这样做):

// 危险!直接把用户输入拼接到 SQL 里
let name = request.body.name;
db.query("SELECT * FROM users WHERE name = '" + name + "'");

如果用户输入的是一个普通名字如 张三,没问题。但如果用户恶意输入了 ' OR '1'='1,拼接出来的 SQL 就变成了:

SELECT * FROM users WHERE name = '' OR '1'='1'

'1'='1' 永远为真,这个条件会匹配所有用户。攻击者可以绕过登录验证、窃取全部数据、甚至删除整张表。这就是著名的 SQL 注入攻击

正确写法(参数化查询):

db.query('SELECT * FROM users WHERE name = ?', [name]);

数据库驱动会把 ? 当作占位符,把 name 的值安全地替换进去。它会自动对特殊字符进行转义处理,确保用户输入的内容只被当作数据,永远不会被当作 SQL 代码执行。

记住一条原则:永远用参数化查询,永远不要拼接 SQL 字符串。

九、文件存储 vs 数据库对比

对比维度 JSON 文件存储 MySQL 数据库
安装配置 零配置 需要安装
数据量 小数据量(几千条) 大数据量(百万条)
查询速度 遍历全部数据,慢 索引加速,快
并发写入 可能冲突 数据库自动处理
条件查询 手动写代码筛选 WHERE 语句
数据完整性 手动验证 NOT NULL、UNIQUE 等约束
适用场景 学习、配置、小型原型 生产环境

十、本篇动手练习

练习 1:用 MySQL 命令行练习 CRUD

不写代码,打开 MySQL 命令行,用 SQL 语句手动完成以下操作:

  • 往 users 表插入 5 条不同的用户数据。
  • 查询所有邮箱以 @example.com 结尾的用户(提示:WHERE email LIKE '%@example.com')。
  • 更新其中一条记录的名字。
  • 删除其中一条记录。

练习 2:增加文章管理 API

用 MySQL 新建一张 articles 表(字段:idtitlecontentauthor),然后在 Express 中实现文章的增删改查接口。参照 users 表的路由写法。

练习 3:增加搜索接口

在文章 API 中增加 GET /api/articles/search?keyword=xxx,用 SQL 的 LIKE 模糊查询返回标题中包含关键词的文章列表。

十一、本篇小结

这一篇我们从文件存储进化到了真正的数据库:

  • 数据库的概念:数据库 → 表 → 字段(列) → 记录(行),就像 Excel 文件 → Sheet → 表头 → 数据行。
  • MySQL 的安装和基本操作:创建数据库、创建表、查看表结构。
  • CRUD 的 SQL 语句INSERT 插入、SELECT 查询、UPDATE 更新、DELETE 删除。WHERE 用于过滤条件。
  • 主键和自增PRIMARY KEY AUTO_INCREMENT 让每条记录都有唯一 ID,且自动递增。
  • Node.js 连接 MySQLmysql2 模块 + 连接池 + Promise API。
  • 参数化查询:用 ? 占位符和数组传值,防止 SQL 注入攻击。这是后端安全的底线。
  • 数据库 vs 文件存储:数据库在大数据量、并发、查询效率、数据完整性方面都碾压文件存储。

掌握了数据库的增删改查,你的后端能力已经上了一个大台阶。现在你的服务可以处理真实的用户数据,可以接受前端提交的信息并永久保存,可以通过条件查询快速返回结果。下一篇,我们把前后端正式对接起来,做一个完整的用户注册登录系统。

下一篇预告

下一篇——《用户认证——从密码到 Token》:实现用户注册和登录功能,理解密码为什么要加密存储(哈希 + 加盐),学会用 JSON Web Token 实现登录状态管理,让你的 API 能识别“这个请求是谁发的”。

后端零基础入门,每周更新。

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容