一、回顾与本篇目标
上一篇我们用 JSON 文件存储用户数据,实现了完整的增删改查。这是一个重要的里程碑——你的后端服务终于有了“记忆”,重启之后数据还在。
但文件存储有它的天花板:数据量大了读写会慢、想按条件查询只能遍历整个数组、多个请求同时写文件可能出问题。这些问题不是说文件存储不好,而是“用文件做数据库”这件事本来就不是文件的强项。文件的强项是存配置、存日志、存静态内容。数据的增删改查,应该交给专业的工具——数据库。
本篇的目标:
- 理解数据库是什么、为什么需要它
- 安装 MySQL,创建数据库和表
- 学会最核心的 SQL 语句:增删改查
- 用 Node.js 连接 MySQL,把上一篇的文件存储替换成真正的数据库
二、数据库到底是什么
如果你用过 Excel,你就已经理解了数据库 80% 的概念。
一个 Excel 文件就是一个数据库。文件里的每一张工作表(Sheet)就是一张表。表的第一行是列名(字段),下面的每一行是一条记录。
| id | name | |
|---|---|---|
| 1 | 张三 | zhangsan@example.com |
| 2 | 李四 | lisi@example.com |
| 3 | 王五 | wangwu@example.com |
翻译成数据库术语:
- 这个 Excel 文件 → 数据库
- 这张 Sheet → 表
- 第一行的 id、name、email → 字段(列)
- 下面的每一行 → 记录(行)
数据库和 Excel 的区别在于:数据库不是给人手动翻看的,而是给程序高速读写的。它能同时处理成千上万个请求,能对百万级的数据做亚秒级查询,能保证数据不会因为断电而损坏。这些 Excel 做不到。
MySQL 是世界上最流行的关系型数据库之一。“关系型”的意思就是数据以表格的形式组织,表和表之间可以通过字段关联(后面会学到)。它免费、开源、稳定,是学习数据库的最佳起点。
三、安装 MySQL
Windows 安装
- 打开浏览器,访问
https://dev.mysql.com/downloads/mysql/。 - 找到 MySQL Community Server,选择 Windows 平台,下载 MSI Installer 版本。
- 双击下载好的安装包,选择 Custom 安装类型。
- 在安装组件列表中,确保勾选了 MySQL Server。
- 一路点 Next,设置 root 用户的密码时,设一个你记得住的密码(比如
123456,学习阶段简单点没关系)。 - 完成安装。
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;
同样要小心:不加 WHERE 的 DELETE 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 表(字段:id、title、content、author),然后在 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 连接 MySQL:
mysql2模块 + 连接池 + Promise API。 - 参数化查询:用
?占位符和数组传值,防止 SQL 注入攻击。这是后端安全的底线。 - 数据库 vs 文件存储:数据库在大数据量、并发、查询效率、数据完整性方面都碾压文件存储。
掌握了数据库的增删改查,你的后端能力已经上了一个大台阶。现在你的服务可以处理真实的用户数据,可以接受前端提交的信息并永久保存,可以通过条件查询快速返回结果。下一篇,我们把前后端正式对接起来,做一个完整的用户注册登录系统。
下一篇预告
下一篇——《用户认证——从密码到 Token》:实现用户注册和登录功能,理解密码为什么要加密存储(哈希 + 加盐),学会用 JSON Web Token 实现登录状态管理,让你的 API 能识别“这个请求是谁发的”。
后端零基础入门,每周更新。













暂无评论内容