
今天,我们来一起动手,使用 Node.js 连接 MySQL 数据库,构建一个功能完整的图书信息管理系统。我们将涵盖从数据库设计、表结构创建,到使用 Express 框架实现增删改查(CRUD)接口,并加入一些实用的统计分析功能。文章末尾会提供完整的、可运行的项目代码。
在 AI 编程工具日益普及的当下,深入理解底层技术原理和整体架构设计思维,反而是开发者构筑自身竞争力的关键。希望通过这个具体的实战项目,能帮助你夯实后端开发的基础。
一、数据库基础概念
在编写代码之前,我们先快速回顾几个核心的数据库概念,便于后续的理解。
-
表、行、列
- 表(Table): 存储特定类型数据的集合,例如
books(书籍表)。
- 行(Row): 表中的一条记录,代表一个具体实体,如一本具体的书。
- 列(Column): 表中的一个字段,代表实体的某个属性,如书名、作者。
-
主键与外键
- 主键(Primary Key): 唯一标识表中每一行数据的字段(如书籍ID),不可重复且不能为空。
- 外键(Foreign Key): 用于建立表与表之间关联的字段。例如,书籍表中的
author_id 关联到作者表的主键 id。
-
常用数据类型
INT: 整数。
VARCHAR(长度): 可变长度字符串。
TEXT: 长文本。
DATE / DATETIME: 日期或日期时间。
DECIMAL: 精确小数。
二、设计数据库表结构
我们的图书管理系统至少需要两张表:作者表(authors) 和 书籍表(books)。一个作者可以对应多本书,构成一对多关系。
1. 创建数据库
首先,创建一个支持中文和特殊字符的数据库。
CREATE DATABASE book_manager CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
2. 创建作者表(authors)
| 字段名 |
类型 |
约束 |
说明 |
| id |
INT |
PRIMARY KEY AUTO_INCREMENT |
作者ID,自增主键 |
| name |
VARCHAR(50) |
NOT NULL |
作者姓名 |
| bio |
TEXT |
|
作者简介 |
| created_at |
DATETIME |
DEFAULT CURRENT_TIMESTAMP |
记录创建时间 |
对应的建表 SQL 如下:
CREATE TABLE authors (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
bio TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
3. 创建书籍表(books)
| 字段名 |
类型 |
约束 |
说明 |
| id |
INT |
PRIMARY KEY AUTO_INCREMENT |
书籍ID |
| title |
VARCHAR(100) |
NOT NULL |
书名 |
| author_id |
INT |
NOT NULL |
作者ID(外键) |
| published_date |
DATE |
|
出版日期 |
| pages |
INT |
|
页数 |
| reads |
INT |
DEFAULT 0 |
阅读量 |
| created_at |
DATETIME |
DEFAULT CURRENT_TIMESTAMP |
记录创建时间 |
对应的建表 SQL 如下,注意我们定义了外键约束:
CREATE TABLE books (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(100) NOT NULL,
author_id INT NOT NULL,
published_date DATE,
pages INT,
reads INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE CASCADE
);
ON DELETE CASCADE 表示当删除一个作者时,其关联的所有书籍记录也会被自动删除。你可以根据实际业务需求将其改为 SET NULL 或 RESTRICT。
三、基础数据操作(SQL示例)
表建好后,我们可以通过 SQL 语句进行最基本的增删改查操作。
-
插入数据
-- 插入作者
INSERT INTO authors (name, bio) VALUES ('东野圭吾', '日本推理小说作家');
-- 插入书籍
INSERT INTO books (title, author_id, published_date, pages, reads)
VALUES ('白夜行', 1, '1999-08-01', 538, 12000);
-
查询数据
-- 关联查询书籍及其作者
SELECT books.id, books.title, authors.name AS author, books.reads
FROM books
JOIN authors ON books.author_id = authors.id;
-
更新数据
-- 为ID为1的书籍增加100阅读量
UPDATE books SET reads = reads + 100 WHERE id = 1;
-
删除数据
-- 删除ID为1的书籍
DELETE FROM books WHERE id = 1;
四、使用 Node.js 连接 MySQL
我们选择 mysql2 这个库,它支持 Promise API,与现代的 async/await 语法配合使用非常方便。
1. 初始化项目
mkdir book-manager
cd book-manager
npm init -y
npm install express mysql2
2. 创建数据库连接文件 db.js
使用连接池(Pool)是推荐的做法,它可以有效管理数据库连接,提升性能。
const mysql = require('mysql2');
// 创建连接池
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'yourpassword', // 请替换为你的MySQL密码
database: 'book_manager',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
charset: 'utf8mb4' // 确保字符集一致,避免乱码
});
// 导出 Promise 版本的连接池
module.exports = pool.promise();
五、实现图书管理的 RESTful API
我们将使用 Express 框架来构建一个简单的 HTTP 服务器,并提供一组完整的 RESTful API。
1. 创建主应用文件 app.js 骨架
const express = require('express');
const db = require('./db'); // 导入上面创建的数据库模块
const app = express();
app.use(express.json()); // 用于解析 JSON 格式的请求体
const PORT = 3000;
// API 接口将在这里添加...
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
2. 创建书籍(Create - POST)
// POST /books
app.post('/books', async (req, res) => {
const { title, author_id, published_date, pages, reads } = req.body;
// 简单的参数校验
if (!title || !author_id) {
return res.status(400).json({ error: '标题和作者ID不能为空' });
}
try {
const sql = `INSERT INTO books (title, author_id, published_date, pages, reads) VALUES (?, ?, ?, ?, ?)`;
const [result] = await db.execute(sql, [title, author_id, published_date, pages, reads || 0]);
res.status(201).json({ id: result.insertId, ...req.body });
} catch (err) {
console.error(err);
res.status(500).json({ error: '数据库错误' });
}
});
3. 查询书籍列表(Read - GET)
// GET /books
app.get('/books', async (req, res) => {
try {
const sql = `SELECT books.*, authors.name AS author_name
FROM books
JOIN authors ON books.author_id = authors.id
ORDER BY books.created_at DESC`;
const [rows] = await db.query(sql);
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: '数据库错误' });
}
});
4. 查询单本书籍详情
// GET /books/:id
app.get('/books/:id', async (req, res) => {
const { id } = req.params;
try {
const sql = `SELECT books.*, authors.name AS author_name
FROM books
JOIN authors ON books.author_id = authors.id
WHERE books.id = ?`;
const [rows] = await db.execute(sql, [id]);
if (rows.length === 0) {
return res.status(404).json({ error: '书籍不存在' });
}
res.json(rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: '数据库错误' });
}
});
5. 更新书籍信息(Update - PUT)
// PUT /books/:id
app.put('/books/:id', async (req, res) => {
const { id } = req.params;
const { title, author_id, published_date, pages, reads } = req.body;
try {
// 先检查书籍是否存在
const [check] = await db.execute('SELECT id FROM books WHERE id = ?', [id]);
if (check.length === 0) {
return res.status(404).json({ error: '书籍不存在' });
}
const sql = `UPDATE books
SET title = ?, author_id = ?, published_date = ?, pages = ?, reads = ?
WHERE id = ?`;
await db.execute(sql, [title, author_id, published_date, pages, reads, id]);
res.json({ message: '更新成功' });
} catch (err) {
console.error(err);
res.status(500).json({ error: '数据库错误' });
}
});
6. 删除书籍(Delete - DELETE)
// DELETE /books/:id
app.delete('/books/:id', async (req, res) => {
const { id } = req.params;
try {
const [result] = await db.execute('DELETE FROM books WHERE id = ?', [id]);
if (result.affectedRows === 0) {
return res.status(404).json({ error: '书籍不存在' });
}
res.json({ message: '删除成功' });
} catch (err) {
console.error(err);
res.status(500).json({ error: '数据库错误' });
}
});
六、实现统计分析功能
除了基础的 CRUD,我们还可以提供一些数据统计接口。
1. 统计每位作者的总阅读量
// GET /authors/stats/reads
app.get('/authors/stats/reads', async (req, res) => {
try {
const sql = `
SELECT authors.id, authors.name, SUM(books.reads) AS total_reads
FROM authors
LEFT JOIN books ON authors.id = books.author_id
GROUP BY authors.id
ORDER BY total_reads DESC
`;
const [rows] = await db.query(sql);
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: '数据库错误' });
}
});
2. 获取阅读量最高的 N 本书
// GET /books/top?limit=5
app.get('/books/top', async (req, res) => {
const limit = parseInt(req.query.limit) || 5;
try {
const sql = `
SELECT books.title, authors.name AS author, books.reads
FROM books
JOIN authors ON books.author_id = authors.id
ORDER BY books.reads DESC
LIMIT ?
`;
const [rows] = await db.execute(sql, [limit]);
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: '数据库错误' });
}
});
3. 书籍数据摘要统计
// GET /books/stats/summary
app.get('/books/stats/summary', async (req, res) => {
try {
const sql = `
SELECT
COUNT(*) AS total_books,
AVG(reads) AS avg_reads,
MAX(reads) AS max_reads,
MIN(reads) AS min_reads
FROM books
`;
const [rows] = await db.query(sql);
res.json(rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: '数据库错误' });
}
});
七、常见问题与解决方案
在实际开发中,你可能会遇到以下问题:
-
数据库连接失败
- 现象:
Error: connect ECONNREFUSED 或 Access denied。
- 解决:检查MySQL服务是否启动,确认连接配置(主机、用户名、密码)是否正确。
-
SQL注入风险
- 原因:直接拼接用户输入到 SQL 语句中。
- 解决:永远使用参数化查询(如本文示例中的
? 占位符和 db.execute 方法)。mysql2 库会负责参数的转义。
-
连接未释放导致连接池耗尽
- 现象:运行一段时间后请求卡住。
- 解决:使用
pool.promise() 后,db.query/db.execute 会自动管理连接生命周期。如果手动调用 pool.getConnection(),务必记得在操作完成后 connection.release()。
-
中文字符乱码
- 解决:确保数据库、表和连接三处字符集统一为
utf8mb4。在创建连接池时设置 charset: 'utf8mb4'。
-
事务处理
- 场景:需要确保多个数据库操作要么全部成功,要么全部失败。
- 示例:
const connection = await db.getConnection();
try {
await connection.beginTransaction();
await connection.execute('INSERT INTO books ...', []);
await connection.execute('UPDATE authors SET book_count = book_count + 1 WHERE id = ?', [author_id]);
await connection.commit();
connection.release();
res.json({ success: true });
} catch (err) {
await connection.rollback();
connection.release();
res.status(500).json({ error: '操作失败' });
}
八、完整项目代码与初始化脚本
项目结构
book-manager/
├── package.json
├── db.js
└── app.js
package.json
{
"name": "book-manager",
"version": "1.0.0",
"main": "app.js",
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
},
"dependencies": {
"express": "^4.18.2",
"mysql2": "^3.5.1"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}
数据库初始化脚本 init.sql
将以下SQL在 MySQL 客户端中执行,一键创建数据库、表并插入测试数据。
-- 创建数据库
CREATE DATABASE IF NOT EXISTS book_manager CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE book_manager;
-- 创建作者表
CREATE TABLE IF NOT EXISTS authors (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
bio TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 创建书籍表
CREATE TABLE IF NOT EXISTS books (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(100) NOT NULL,
author_id INT NOT NULL,
published_date DATE,
pages INT,
reads INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE CASCADE
);
-- 插入测试作者数据
INSERT INTO authors (name, bio) VALUES
('东野圭吾', '日本推理小说作家'),
('村上春树', '日本当代作家'),
('刘慈欣', '中国科幻作家');
-- 插入测试书籍数据
INSERT INTO books (title, author_id, published_date, pages, reads) VALUES
('白夜行', 1, '1999-08-01', 538, 12000),
('解忧杂货店', 1, '2012-03-05', 291, 8000),
('挪威的森林', 2, '1987-09-04', 384, 15000),
('三体', 3, '2008-01-01', 302, 20000),
('三体II·黑暗森林', 3, '2008-05-01', 470, 18000);
表关系示意图
+----------------+ +----------------+
| authors | | books |
+----------------+ +----------------+
| id (PK) |<---------| author_id (FK) |
| name | | id (PK) |
| bio | | title |
| created_at | | published_date |
+----------------+ | pages |
| reads |
| created_at |
+----------------+
(箭头表示一对多关系)
总结
通过这个项目,我们完整实践了使用 Node.js 和 MySQL 开发后端服务的关键步骤:数据库设计、连接配置、RESTful API 实现以及错误处理。代码中刻意强调了防止 SQL 注入、使用连接池、统一字符集等工程化实践,这些都是构建稳健后端服务的基础。
你可以将本文提供的代码作为起点,在此基础上增加用户认证、更复杂的查询、分页、文件上传等功能。开发过程中遇到问题,善用搜索引擎和官方文档是成长最快的途径。如果你对更多 Node.js 后端实践或 数据库 优化技巧感兴趣,欢迎在 云栈社区 的相关板块进行深度探讨。
