找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3770

积分

0

好友

530

主题
发表于 12 小时前 | 查看: 2| 回复: 0

动图:分享、收藏、点赞、踩等交互图标演变

今天,我们来一起动手,使用 Node.js 连接 MySQL 数据库,构建一个功能完整的图书信息管理系统。我们将涵盖从数据库设计、表结构创建,到使用 Express 框架实现增删改查(CRUD)接口,并加入一些实用的统计分析功能。文章末尾会提供完整的、可运行的项目代码。

在 AI 编程工具日益普及的当下,深入理解底层技术原理和整体架构设计思维,反而是开发者构筑自身竞争力的关键。希望通过这个具体的实战项目,能帮助你夯实后端开发的基础。

一、数据库基础概念

在编写代码之前,我们先快速回顾几个核心的数据库概念,便于后续的理解。

  1. 表、行、列

    • 表(Table): 存储特定类型数据的集合,例如 books(书籍表)。
    • 行(Row): 表中的一条记录,代表一个具体实体,如一本具体的书。
    • 列(Column): 表中的一个字段,代表实体的某个属性,如书名、作者。
  2. 主键与外键

    • 主键(Primary Key): 唯一标识表中每一行数据的字段(如书籍ID),不可重复且不能为空。
    • 外键(Foreign Key): 用于建立表与表之间关联的字段。例如,书籍表中的 author_id 关联到作者表的主键 id
  3. 常用数据类型

    • 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 NULLRESTRICT

三、基础数据操作(SQL示例)

表建好后,我们可以通过 SQL 语句进行最基本的增删改查操作。

  1. 插入数据

    -- 插入作者
    INSERT INTO authors (name, bio) VALUES ('东野圭吾', '日本推理小说作家');
    -- 插入书籍
    INSERT INTO books (title, author_id, published_date, pages, reads)
    VALUES ('白夜行', 1, '1999-08-01', 538, 12000);
  2. 查询数据

    -- 关联查询书籍及其作者
    SELECT books.id, books.title, authors.name AS author, books.reads
    FROM books
    JOIN authors ON books.author_id = authors.id;
  3. 更新数据

    -- 为ID为1的书籍增加100阅读量
    UPDATE books SET reads = reads + 100 WHERE id = 1;
  4. 删除数据

    -- 删除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: '数据库错误' });
  }
});

七、常见问题与解决方案

在实际开发中,你可能会遇到以下问题:

  1. 数据库连接失败

    • 现象Error: connect ECONNREFUSEDAccess denied
    • 解决:检查MySQL服务是否启动,确认连接配置(主机、用户名、密码)是否正确。
  2. SQL注入风险

    • 原因:直接拼接用户输入到 SQL 语句中。
    • 解决永远使用参数化查询(如本文示例中的 ? 占位符和 db.execute 方法)。mysql2 库会负责参数的转义。
  3. 连接未释放导致连接池耗尽

    • 现象:运行一段时间后请求卡住。
    • 解决:使用 pool.promise() 后,db.query/db.execute 会自动管理连接生命周期。如果手动调用 pool.getConnection(),务必记得在操作完成后 connection.release()
  4. 中文字符乱码

    • 解决:确保数据库、表和连接三处字符集统一为 utf8mb4。在创建连接池时设置 charset: 'utf8mb4'
  5. 事务处理

    • 场景:需要确保多个数据库操作要么全部成功,要么全部失败。
    • 示例
      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.jsMySQL 开发后端服务的关键步骤:数据库设计、连接配置、RESTful API 实现以及错误处理。代码中刻意强调了防止 SQL 注入、使用连接池、统一字符集等工程化实践,这些都是构建稳健后端服务的基础。

你可以将本文提供的代码作为起点,在此基础上增加用户认证、更复杂的查询、分页、文件上传等功能。开发过程中遇到问题,善用搜索引擎和官方文档是成长最快的途径。如果你对更多 Node.js 后端实践或 数据库 优化技巧感兴趣,欢迎在 云栈社区 的相关板块进行深度探讨。

动图:卡通熊猫邀请关注




上一篇:Anthropic指控中国大模型“模型蒸馏”,数据来源与安全争议升级
下一篇:AI编程新范式:从写代码到设计工作流与构建可复用资产
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-25 19:37 , Processed in 0.472746 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表