☰
数据库课程设计图书管理系统:表结构、事务与避坑指南
2026/9/26 0:40:59 网站建设 项目流程

简介:这是一份数据库课程设计“图书管理系统”的完整课程设计报告,面向需要完成数据库课程设计或图书管理相关项目的学生与开发者。系统针对传统人工管理图书信息存在的效率低、易出错、资源浪费等问题,提出并实现了图书集中统一管理方案,覆盖读者信息、书籍类别、书籍库存、借书还书以及超期罚款等核心模块。资源为1个doc文档,约122KB,正文从背景与需求分析切入,详细给出实体关系模式、多张E-R图以及书籍类别、读者、书籍、借阅、归还、罚款等数据字典定义,并说明系统实现要点。目前已有1897人学习下载。报告结构完整、设计思路清晰,既可直接作为课程设计文档模板,也可为小型图书管理系统的表结构设计、功能划分和文档撰写提供实用参考。

1. 数据库课程设计图书管理系统:先过表结构,再谈界面

图书管理系统是数据库课程设计里最经典也最容易“看起来简单、答辩翻车”的题目。很多同学把功夫花在 Swing 界面和按钮上,结果老师一句“借书时如果两个人同时借同一本书怎么办”就卡住了。这个数据库课设真正考的不是界面多精致,而是外键约束、事务边界和联表查询经不经得起追问。这篇按做课设的完整链路来:表结构设计、JDBC 连接层、借还书事务、答辩前自测,把能直接复用的方案和踩过的坑一起讲清楚,适合正在凑功能又怕上台被问住的人。

2. 从“借书”和“还书”倒推表结构:五张表、两处约束、一组验证 SQL

2.1 最小可行模型:分类、图书、读者、借阅和管理员五张表

课程设计要求里最常出现的功能是:图书增删改查、读者增删改查、借书还书、逾期统计。从这些操作倒推,最少需要五张表:分类表、图书表、读者表、借阅记录表、管理员表。图书分类单独建表而不是在图书表里存一个字符串,是为了避免“改一个分类名就得 UPDATE 所有图书”的尴尬,答辩问第三范式时也能讲清楚。

CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE, sort INT NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL, title VARCHAR(200) NOT NULL, author VARCHAR(100) NOT NULL, publisher VARCHAR(100), category_id INT, total INT NOT NULL DEFAULT 1 COMMENT '馆藏总量', available INT NOT NULL DEFAULT 1 COMMENT '当前可借数量', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_book_category FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT, reader_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号/工号', name VARCHAR(50) NOT NULL, type TINYINT NOT NULL DEFAULT 1 COMMENT '1-学生 2-教师', max_borrow INT NOT NULL DEFAULT 5, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE borrow ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_time DATETIME NOT NULL, due_time DATETIME NOT NULL COMMENT '应还时间', return_time DATETIME DEFAULT NULL COMMENT '实际归还时间', status TINYINT NOT NULL DEFAULT 0 COMMENT '0-借出 1-已还', CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book(id), CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader(id), INDEX idx_borrow_status (status), INDEX idx_borrow_reader_due (reader_id, due_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(128) NOT NULL, role VARCHAR(20) NOT NULL DEFAULT 'operator' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

借阅表是整个设计的关键。book_id 和 reader_id 是两个外键,分别关联图书和读者;status 区分“借出中”和“已归还”。注意这里没有直接存“罚款金额”,因为逾期费是业务计算结果,用”DATEDIFF + 规则“实时算,比存字段更灵活。管理员表的 password_hash 用来存哈希而不是明文密码,答辩时提到这一点会比别人显得细致。

图书表里 total 和 available 是两件事:total 是馆藏总量,available 是当前可借数量。课设里很多人只留一个 total,借出去就不知道还剩几本,所以答辩必被问“你系统里怎么知道哪本书还有没有库存”。用 available 字段记录,借书时减一,还书时加一,查询时直接 SELECT 即可。

2.2 两处容易被忽略的约束:库存字段不是装饰,状态校验要进事务

第一处约束是唯一键。reader 表的 reader_no、admin 表的 username、category 表的 name 都要 UNIQUE。这三处不建唯一索引,程序里靠“先查再插”挡不住并发重复提交,在 MySQL 里也几乎无法靠普通索引兜底,所以必须从表结构层面约束。

第二处约束在借阅表。同一本书(同一个 book_id),同一个读者,不能出现两条 status=0 的记录——不能在未归还的情况下重复借同一本书。MySQL 不支持“部分唯一索引”,没法直接写一条 UNIQUE 约束覆盖“只对 status=0 生效”这种场景。常见做法也不是硬加索引,而是把检查放进借书事务:先查该读者是否存在 status=0 且 book_id 相同的记录,有就拒绝,再用事务保证整个检查+插入的原子性。这个方案的 SQL 写法会在第四章展开。

还有一种更贴近真实图书馆的设计:book 表只存书目信息,额外建一张 book_item 表,每一行代表一本具体的物理书(有自己的馆藏编号和状态)。借书操作变成“把某一本书的 item 状态改成借出”,天然不存在“同一本书重复借”的问题,也不用维护 total/available 两个数字。课设里我用的是前一种汇总库存模式,因为建表少、查询直观;如果指导老师要求“贴近实际业务”,就改成后者,多一张表、多一两个 JOIN,但业务模型更严谨。

2.3 验证设计的三个联表查询:写错 JOIN 类型会在答辩时暴露

表建完后先别急着写界面,用三条 SQL 验证关系是否正确。第一条规定“某读者当前借了哪些书”,第二条规定“某本书的借阅历史和当前可借数”,第三条规定“逾期未还清单”。

-- 查询读者 20210001 当前借了哪些书 SELECT b.title, b.author, br.borrow_time, br.due_time FROM borrow br JOIN reader r ON br.reader_id = r.id JOIN book b ON br.book_id = b.id WHERE r.reader_no = '20210001' AND br.status = 0; -- 查询某本书的借阅历史 SELECT r.reader_no, r.name, br.borrow_time, br.return_time, br.status FROM borrow br JOIN reader r ON br.reader_id = r.id JOIN book b ON br.book_id = b.id WHERE br.book_id = 1 ORDER BY br.borrow_time DESC; -- 逾期未还清单 SELECT r.reader_no, r.name, b.title, br.borrow_time, br.due_time, DATEDIFF(CURDATE(), br.due_time) AS overdue_days FROM borrow br JOIN reader r ON br.reader_id = r.id JOIN book b ON br.book_id = b.id WHERE br.status = 0 AND br.due_time < NOW() ORDER BY overdue_days DESC;

三条查询都用 INNER JOIN,因为借阅记录里的 book_id 和 reader_id 在外键约束下必然存在对应数据,用 LEFT JOIN 反而会让“借阅记录对应到空读者”这种脏数据混进来。逾期查询里“br.due_time < NOW()”判断的是时间点,不要用“CURDATE() < br.due_time”这种日期比较,否则当天到期的晚上还书会被算成第二天。

这三条 SQL 跑通后,表结构基本就立住了。接下来是选技术栈和搭工程,这决定你后面几周写代码的体验。

3. 技术栈与连接层:Java Swing + MySQL 的课设路线,以及连接池要不要上

3.1 选型:Swing、JavaWeb、PHP 怎么选

图书管理系统在数据库课程设计里的技术路线有几种,我见过最多的是 Java Swing + MySQL:写起来直接,一个 JDBC 类就能跑通全部,机房演示不需要装 Tomcat。JavaWeb(Servlet + JSP + MySQL)适合课程名称偏“Web 开发”的班级,但本地要多配一套 Tomcat,答辩现场环境出问题的概率更大。PHP + MySQL 写起来最快,但很多学生不熟悉 PHP 的语法,出了错不好查。

技术路线上手成本演示环境答辩风险
Java Swing + MySQL低本机直接跑,双击 jar 或 IDE 启动界面土,但数据库逻辑好讲
Servlet + JSP + MySQL中需要 Tomcat,端口和路径易出错容易被追问 Web 容器和会话
PHP + MySQL低需要 Apache/Nginx + PHP 环境环境配置翻车率高
C# WinForm + SQLServer低Windows 本机,但 SQLServer 安装重如果学校只教过 MySQL,会被问数据库迁移

我的建议是:哪门语言熟就用哪条,数据库课设不是 Web 课设,不靠框架加分。选好后所有精力放在表设计和事务上,别为了“炫技”去引入 Redis 或 MongoDB,老师一眼就知道这不是数据库课程设计的考察点。

3.2 工程骨架与 JDBC 工具类:把连接配置和业务代码分开

工程结构不需要复杂,按“视图层、数据访问层、模型层、工具类”四块分,目录越多老师越觉得你有工程意识。参考结构:

src/ ├── Main.java // 启动入口 ├── db/DBUtil.java // 数据库连接工具 ├── dao/BookDao.java // 图书表操作 ├── dao/ReaderDao.java ├── dao/BorrowDao.java ├── model/Book.java ├── model/Reader.java ├── model/Borrow.java └── view/LoginFrame.java // 登录界面,后续跳转主界面

连接配置单独放一个 db.properties,不要把数据库地址和密码硬编码在 Java 类里。这样换数据库或改密码时不用重新编译,答辩时被问“如果数据库换到另一台机器怎么办”也能直接回答“改配置就行”。

public class DBUtil { private static String url; private static String username; private static String password; static { try (InputStream in = DBUtil.class.getResourceAsStream("/db.properties")) { Properties props = new Properties(); props.load(in); url = props.getProperty("jdbc.url"); username = props.getProperty("jdbc.username"); password = props.getProperty("jdbc.password"); // 显式注册驱动,兼容 MySQL 5.x 的旧连接串写法 Class.forName("com.mysql.cj.jdbc.Driver"); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } }
jdbc.url=jdbc:mysql://localhost:3306/library_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=123456

URL 里的参数每个都有用。characterEncoding=utf8mb4 解决中文乱码,serverTimezone=Asia/Shanghai 防止数据库时间比本地时间少 8 小时。allowPublicKeyRetrieval=true 是 MySQL 8.0 用 caching_sha2_password 认证时第一次连接必需的,不加会报“Public Key Retrieval is not allowed”,这个坑我帮别人排查过好几次。Driver 类名注意是 com.mysql.cj.jdbc.Driver,旧版的 com.mysql.jdbc.Driver 在 MySQL 8 下会打警告。

3.3 连接池不是必须但值得写:Druid 最小配置和一个常见误区

课程设计的数据量很小,直接用 DriverManager 每次 new 一个连接完全跑得动。但连接池值得写,因为它能在答辩时展示你理解“连接生命周期”这个概念。我用的是阿里 Druid,配置比 HikariCP 直观。

driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/library_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false username=root password=123456 initialSize=3 minIdle=3 maxActive=10 maxWait=60000
public class DBManager { private static DruidDataSource dataSource; static { try (InputStream in = DBManager.class.getResourceAsStream("/druid.properties")) { Properties props = new Properties(); props.load(in); dataSource = new DruidDataSource(); dataSource.setDriverClassName(props.getProperty("driverClassName")); dataSource.setUrl(props.getProperty("url")); dataSource.setUsername(props.getProperty("username")); dataSource.setPassword(props.getProperty("password")); dataSource.setInitialSize(Integer.parseInt(props.getProperty("initialSize", "3"))); dataSource.setMaxActive(Integer.parseInt(props.getProperty("maxActive", "10"))); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }

initialSize 是启动时就建立的连接数,maxActive 是连接池最多能同时打开的连接数,maxWait 是拿不到连接时最长等待毫秒数。课设阶段 initialSize=3、maxActive=10 足够。

用了连接池之后最大的误区是“close 变成了归还连接”。很多人用连接池后忘记在 finally 里 close,最后把连接池耗空报 Too many connections。这个坑单独放到第五章讲。

4. 把增删改查跑通:图书管理系统的三个关键功能和一组事务代码

4.1 添加图书:PreparedStatement 与参数校验的位置

数据库课程设计的“增删改查”里,添加图书是第一个要写完整的功能。网上很多教程直接用字符串拼接 SQL,这在课设里能跑,但老师问“如何防止 SQL 注入”时就会卡壳。正确写法是用 PreparedStatement,参数用 ? 占位,由 JDBC 驱动负责转义。

public int insert(Book book) throws SQLException { if (book == null || book.getTitle() == null || book.getTitle().isBlank()) { throw new IllegalArgumentException("书名不能为空"); } if (book.getTotal() < 0 || book.getAvailable() < 0) { throw new IllegalArgumentException("库存数量不能为负数"); } String sql = "INSERT INTO book (isbn, title, author, publisher, category_id, total, available) " + "VALUES (?, ?, ?, ?, ?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, book.getIsbn()); ps.setString(2, book.getTitle()); ps.setString(3, book.getAuthor()); ps.setString(4, book.getPublisher()); if (book.getCategoryId() == null) { ps.setNull(5, Types.INTEGER); } else { ps.setInt(5, book.getCategoryId()); } ps.setInt(6, book.getTotal()); ps.setInt(7, book.getAvailable()); ps.executeUpdate(); try (ResultSet keys = ps.getGeneratedKeys()) { if (keys.next()) { return keys.getInt(1); } } return 0; } }

参数校验放在 DAO 层而不是界面层,是因为 DAO 层之外的调用方(比如以后加了一个批量导入 Excel 的功能)也会走这个方法。total 和 available 的初始值在正常录入时是相等的,但如果有“书籍上架前先登记总数、再分批入库”的业务,这两个值就可以不同。

4.2 借书是两条更新的组合:事务、行锁和回滚

借书功能是所有课设里最值得认真写的部分。表面上是 INSERT 一条借阅记录,实际顺序是:查读者是否超量,查图书是否可借,扣减 available,插入借阅记录。这四个步骤中间任何一步失败,前面都不能生效,否则会出现“库存扣了但借阅记录没插上”或反过来“记录插了但库存没扣”的脏数据。这就是事务的典型场景。

public void borrow(int bookId, int readerId) throws SQLException { Connection conn = DBUtil.getConnection(); try { conn.setAutoCommit(false); // 1. 锁定读者行,串行化对同一读者的并发借书 String sqlReader = "SELECT max_borrow FROM reader WHERE id = ? FOR UPDATE"; int maxBorrow; try (PreparedStatement ps = conn.prepareStatement(sqlReader)) { ps.setInt(1, readerId); try (ResultSet rs = ps.executeQuery()) { if (!rs.next()) { throw new SQLException("读者不存在"); } maxBorrow = rs.getInt("max_borrow"); } } // 2. 统计当前未还数量 String countSql = "SELECT COUNT(*) FROM borrow WHERE reader_id = ? AND status = 0"; int borrowingCount; try (PreparedStatement ps = conn.prepareStatement(countSql)) { ps.setInt(1, readerId); try (ResultSet rs = ps.executeQuery()) { rs.next(); borrowingCount = rs.getInt(1); } } if (borrowingCount >= maxBorrow) { throw new SQLException("已达到最大借阅数量"); } // 3. 锁定图书行并检查可借库存 String sqlBook = "SELECT available FROM book WHERE id = ? FOR UPDATE"; int available; try (PreparedStatement ps = conn.prepareStatement(sqlBook)) { ps.setInt(1, bookId); try (ResultSet rs = ps.executeQuery()) { if (!rs.next()) { throw new SQLException("图书不存在"); } available = rs.getInt("available"); } } if (available <= 0) { throw new SQLException("图书已全部借出"); } // 4. 扣减库存,带 available > 0 条件防并发超借 String updateBook = "UPDATE book SET available = available - 1 WHERE id = ? AND available > 0"; int rows; try (PreparedStatement ps = conn.prepareStatement(updateBook)) { ps.setInt(1, bookId); rows = ps.executeUpdate(); } if (rows == 0) { throw new SQLException("库存已被抢占,请重试"); } // 5. 插入借阅记录,应还时间 = 借出时间 + 30 天 String insertBorrow = "INSERT INTO borrow (book_id, reader_id, borrow_time, due_time, status) " + "VALUES (?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY), 0)"; try (PreparedStatement ps = conn.prepareStatement(insertBorrow)) { ps.setInt(1, bookId); ps.setInt(2, readerId); ps.executeUpdate(); } conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } }

这段代码里的 FOR UPDATE 是行锁,把读者行和图书行锁住,让两个并发借书请求不能同时读到同一份可用库存,避免超借。课设里讲解“为什么用 FOR UPDATE”是个很好的加分点。UPDATE 语句里带“available > 0”再加受影响行数判断,是第二重保险。

finally 里的“conn.setAutoCommit(true)”很多人会漏。如果连接是借来的,事务结束后恢复自动提交是必须的,否则下一次拿到同一连接时事务边界就乱了。还有一点注意:setAutoCommit(false) 必须在 try 之前调用,如果放进 try 里,一旦第一步就抛异常,conn 回滚时可能还没开启事务。

4.3 逾期统计:DATEDIFF 在还书场景的准确用法

逾期天数可以在 SQL 里算,也可以在 Java 里算。如果只是展示统计报表,SQL 里直接用 DATEDIFF 最省事;如果要算逾期费并且打印到凭条上,我建议在事务方法里算,因为 SQL 的 DATEDIFF 比较的是日期部分,会把“当天到期当天还”也算成 0 天,规则更可控。

SELECT br.id, r.name AS reader_name, b.title AS book_title, br.borrow_time, br.due_time, DATEDIFF(CURDATE(), br.due_time) AS overdue_days FROM borrow br JOIN reader r ON br.reader_id = r.id JOIN book b ON br.book_id = b.id WHERE br.status = 0 AND br.due_time < NOW() ORDER BY overdue_days DESC;

还书方法的逻辑刚好反着:先查借阅记录,再算逾期天数,更新借阅状态后回补库存。借阅记录的状态更新一定要带“WHERE id = ? AND status = 0”,防止同一张借阅单被重复点击还书导致库存多加一次。

public ReturnResult returnBook(int borrowId) throws SQLException { Connection conn = DBUtil.getConnection(); try { conn.setAutoCommit(false); String querySql = "SELECT book_id, due_time FROM borrow WHERE id = ? AND status = 0 FOR UPDATE"; int bookId; LocalDateTime dueTime; try (PreparedStatement ps = conn.prepareStatement(querySql)) { ps.setInt(1, borrowId); try (ResultSet rs = ps.executeQuery()) { if (!rs.next()) { throw new SQLException("借阅记录不存在或已归还"); } bookId = rs.getInt("book_id"); dueTime = rs.getTimestamp("due_time").toLocalDateTime(); } } long overdueDays = Duration.between(dueTime, LocalDateTime.now()).toDays(); if (overdueDays < 0) { overdueDays = 0; } try (PreparedStatement ps = conn.prepareStatement( "UPDATE borrow SET status = 1, return_time = NOW() WHERE id = ? AND status = 0")) { ps.setInt(1, borrowId); ps.executeUpdate(); } try (PreparedStatement ps = conn.prepareStatement( "UPDATE book SET available = available + 1 WHERE id = ?")) { ps.setInt(1, bookId); ps.executeUpdate(); } conn.commit(); return new ReturnResult(borrowId, overdueDays); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } }

Duration.between(...).toDays() 是向下取整,比如到期时间是昨天,现在是今天凌晨,算出来可能是 0 天。这不算 bug,是规则问题。答辩时你要能说出“按自然日计算,还是按整 24 小时计算,我们系统采用了哪种”——别小看这个细节,很多老师就爱问边界日期。

5. 课设避坑指南:五个让图书管理系统当场翻车的高频陷阱

5.1 外键约束删不掉:删除分类或图书总报约束失败

现象:界面上点“删除”时程序报错,错误信息里有“Cannot delete or update a parent row: a foreign key constraint fails”。

原因:分类表被图书表的 category_id 外键引用,图书表又被借阅表引用,只要存在一条借阅记录,相关图书就不能硬删。

解决:两个方向。一是改成逻辑删除,给分类表和图书表加 is_deleted 字段(TINYINT DEFAULT 0),删除操作变成 UPDATE is_deleted = 1,查询时统一带“AND is_deleted = 0”。二是物理删除前先删子表记录,这会连带删掉借阅历史,不适合需要保留日志的课设。常见做法是用逻辑删除,答辩时还能顺带讲“为什么不能直接删:历史借阅记录是审计数据”。

5.2 中文乱码:界面上显示的“计算机”变成“????”

现象:数据库里存的中文正常,Java 界面里显示乱码;或者反过来,程序里写入数据库后变成问号。

原因:三种情况最容易混在一起。连接 URL 没带 characterEncoding=utf8mb4;数据库库表字符集是 latin1;IDE 控制台的编码不是 UTF-8。

解决:先统一三处字符集。建库用“CREATE DATABASE library_db DEFAULT CHARSET utf8mb4”,连接 URL 带上 useUnicode=true&characterEncoding=utf8mb4,IDE 的 VM 参数加“-Dfile.encoding=UTF-8”。如果表已经建错,用“ALTER TABLE book CONVERT TO CHARACTER SET utf8mb4;”补救。排查顺序建议先用 Navicat 直接看库里数据,确认数据库端正常后再查 Java 端,避免两头猜。

5.3 逾期天数差一天:到期当天还书算不算逾期

现象:同一本书,上午还和下午还,逾期天数一个是 1 天一个是 0 天,或者完全相反。

原因:DATEDIFF 按日期边界计算,只比较“哪天”,不比较“几点”。如果借书时间包含时分秒,而应还时间也是时分秒,那“到期当天”这个说法本身就有两种规则。

解决:提前定规则并写进代码注释。我一般用“超过应还时间点即算逾期,按自然日向下取整”的规则,即 Duration.toDays() 的结果为正数才收逾期费,0 或负数都不收。这个规则要写进系统说明文档,答辩时主动讲出来,老师会觉得你考虑过边界。

5.4 程序连不上表:报“Table doesn't exist”但 Navicat 里明明能看到

现象:Navicat 打开同一个数据库能看到 book 表,程序查询却说表不存在。

原因:最常见的是程序连的库不是你以为的库。MySQL 里大小写规则在 Linux 和 Windows 上不同,Linux 下表名区分大小写,Windows 不区分。如果建表脚本写的“Book”而查询写的“book”,Windows 上没事,换到 Linux 的服务器上就报错。

解决:统一用小写表名,SQL 里字段名也全部小写。另外在 JDBC 连接成功之后执行一句“SELECT DATABASE()”确认当前库名,很多同学本地装了两个 MySQL 实例,程序连的 3306 端口和 Navicat 连的根本不是同一个库,这种问题最容易让人怀疑人生。

5.5 Too many connections:运行一会儿就报连接数超限

现象:系统刚启动正常,点几次借书还书后报“Too many connections”。用连接池时也偶发。

原因:写 DAO 时只关了 ResultSet 和 Statement,忘了关 Connection。用 DriverManager 直连时,连接用完不关就会一直占用;用连接池时,Connection.close() 是归还连接而不是关闭物理连接,不调用 close 就等于把池里的连接全部占死。

解决:所有 DAO 方法都改成 try-with-resources,Connection 放在 try 小括号里就能自动关闭。连接池场景下还可以在 Druid 的配置文件里加一条 testOnBorrow=true,保证每次拿到的连接可用。排查时用“SHOW PROCESSLIST;”看连接状态,能直观看到哪些连接是 Sleep 状态一直没还。

6. 答辩前的自测与演示数据:三组用例和两段必会 SQL

6.1 三组必测用例

答辩现场最容易出的问题不是功能没有,而是演示顺序乱了。我习惯提前把系统按三类用例过一遍,每一类都有明确的预期结果。

用例类型操作步骤预期结果
正常借还选一本书,点借出,再点归还借出时 available 减一,归还时 available 加一,借阅记录 status 变为 1
超量拒绝用一个达到 max_borrow 上限的读者继续借书系统提示“已达到最大借阅数量”,不插入新记录
逾期处理先跑逾期清单,再对一条逾期记录点还书清单显示 overdue_days,还书后返回逾期天数并扣回库存

第一组用例随便做,第二组和第三组才是让系统显得完整的关键。如果第二组只能靠改数据库数据才能复现,说明你借书代码里漏了超量检查,趁答辩前赶紧补上。

6.2 演示数据与两段必会 SQL

演示数据要故意构造一条逾期记录,否则老师问“逾期逻辑在哪看”时你会现场造数据。推荐直接用下面这组 SQL 初始化:

INSERT INTO category (name) VALUES ('计算机'), ('文学'); INSERT INTO book (isbn, title, author, publisher, category_id, total, available) VALUES ('9787111213826', '深入理解计算机系统', '兰德尔·E·布莱恩特', '机械工业出版社', 1, 3, 1), ('9787020022688', '围城', '钱锺书', '人民文学出版社', 2, 2, 2); INSERT INTO reader (reader_no, name, type, max_borrow) VALUES ('20210001', '张三', 1, 5), ('20210002', '李四', 1, 5); INSERT INTO borrow (book_id, reader_id, borrow_time, due_time, return_time, status) VALUES (1, 1, NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY), NULL, 0), (2, 2, DATE_SUB(NOW(), INTERVAL 40 DAY), DATE_SUB(NOW(), INTERVAL 10 DAY), NULL, 0);

第二条借阅记录是故意做的逾期数据:40 天前借出,应还时间是 10 天前,状态仍为 0。演示时先跑逾期清单能看到李四逾期 10 天,再点还书,系统会提示逾期天数并回补库存。

老师现场让你“写一条 SQL 查逾期读者”是最高频的提问,背熟这一段:

SELECT r.reader_no, r.name, COUNT(*) AS borrow_count FROM borrow br JOIN reader r ON br.reader_id = r.id WHERE br.status = 0 AND br.due_time < NOW() GROUP BY r.reader_no, r.name;

如果老师把题改成“查某本书被谁借过”,SQL 换成:

SELECT r.name, br.borrow_time, br.return_time, br.status FROM borrow br JOIN reader r ON br.reader_id = r.id JOIN book b ON br.book_id = b.id WHERE b.title = '深入理解计算机系统';

答辩前我会把这两条 SQL 亲手跑一遍,再对着系统把正常借还和逾期还书的流程各走一次,整个过程不让代码报一个异常。这个动作帮我避过不少次现场翻车。数据库课程设计说到底考的是“你对自己建的表和写的事务有没有把握”,把背熟的 SQL 换成自己真正理解的东西,老师问多深都不虚。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询