ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

数据库课设进阶:四张表+存储过程+事务+索引的完整借阅系统

数据库课设进阶:四张表+存储过程+事务+索引的完整借阅系统 简介数据库课设——图书借阅管理系统是一份面向高校计算机/软件专业学生的数据库课程设计资料包。项目围绕图书借阅场景涵盖数据库设计、关系模型构建、SQL编程、事务处理、安全性权限管理、性能优化及备份恢复等核心知识点并配有可运行的Java源代码、编译后的class文件及开发环境配置。资源包共81个文件以Java源码、class字节码、jar库文件为主另含项目文档doc、数据库文件db/ldf/mdf以及界面预览图jpg压缩包整体大小约11.12MB目录结构清晰便于按模块查阅。包内文档系统阐述了从数据表设计到借阅事务实现的完整思路代码与数据库文件可直接导入调试帮助理解前后端交互与数据库底层逻辑。已有1544人学习适合需要快速完成课程设计、准备答辩或进行数据库实践入门的学生参考。1. 图书借阅管理系统数据库课设里最该做厚的四张表图书借阅管理系统是数据库课程设计里的高频题目也是最容易做薄的题目——很多人交上去的东西只有几张表和几个页面答辩时一问借书时库存怎么扣减、并发下会不会超卖就卡壳。这套课设资源把该有的数据库能力补齐了四张核心表建表脚本、借书还书两个存储过程、两条审计触发器、一组多表联查视图外加 Java 端参数化增删改查封装。它能解决的具体问题很明确库存不准、外键关系混乱、并发超卖、中文乱码、答辩拿不出性能数据。适合正在赶课设的在校生也适合想快速补一遍 MySQL 事务、存储过程、索引的初级开发。下面按建表 → 借还流程 → 查询封装 → 排错 → 压测验证的顺序展开每个环节都给可直接运行的 SQL 和关键参数说明。2. 建表与索引四张表把借阅业务落到外键约束2.1 需求拆分读者、图书、借阅、罚款四个对象很多课设翻车不是死在写不出 SQL而是死在表设计阶段——一张表塞十几个字段借阅记录和读者信息混在一起删一个读者连带把历史借阅也删了。我在拆分表结构时只遵循一条原则每个业务对象一张表历史动作单独落表。这个系统需要覆盖四个对象。读者reader记录借书证号、姓名、联系方式、最大可借册数、当前在借数量和状态图书book记录 ISBN、书名、作者、分类、馆藏总数和可借数量借阅borrow是核心流水表记录谁在什么时候借了哪本书、应还日期和实际还书日期罚款fine由逾期动作产生关联到具体某条借阅记录。四张表的关系很清晰reader 和 book 通过 borrow 建立联系fine 再指向 borrow形成一条完整的业务链。这样拆有两个直接收益。第一历史数据不丢——读者注销了借阅流水还在统计排行榜不受影响第二外键约束真正生效——你没法给一个不存在的读者插入借阅记录也没法随便删掉还有在借图书的 book 行。2.2 核心建表 SQL字段类型、默认值与约束说明CREATE TABLE book ( book_id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 图书ID, isbn VARCHAR(20) NOT NULL COMMENT ISBN号, title VARCHAR(100) NOT NULL COMMENT 书名, author VARCHAR(50) DEFAULT NULL COMMENT 作者, category VARCHAR(30) DEFAULT 未分类 COMMENT 分类, total_count INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 馆藏总数, available_count INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 当前可借数量, price DECIMAL(8,2) DEFAULT 0.00 COMMENT 定价, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 入库时间, PRIMARY KEY (book_id), UNIQUE KEY uk_isbn (isbn), KEY idx_category (category) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表;这段建表 SQL 里有三个设计点是课设答辩常问的。一是 total_count 和 available_count 分开存而不是每次借还都去 COUNT 一遍历史流水——数据量上来之后频繁统计的开销非常明显用冗余字段换查询速度。二是 isbn 加 UNIQUE 约束同一本书录两遍是录入阶段最常见的脏数据唯一索引从根上挡住。三是 category 建了普通索引因为按分类统计是后面的高频查询。CREATE TABLE reader ( reader_id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 读者ID, card_no VARCHAR(20) NOT NULL COMMENT 借书证号, name VARCHAR(30) NOT NULL COMMENT 姓名, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, max_borrow TINYINT UNSIGNED NOT NULL DEFAULT 5 COMMENT 最大可借册数, borrowed_count TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 当前在借册数, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0冻结, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间, PRIMARY KEY (reader_id), UNIQUE KEY uk_card_no (card_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT读者表;reader 表里 max_borrow 和 borrowed_count 这两个字段容易被忽略。借书时先拿 borrowed_count 和 max_borrow 比超了就拒绝这个额度控制是借阅系统的硬需求。status 用 TINYINT 而不是 CHAR 或 BIT是为了留扩展位——1 正常、0 冻结以后要加挂失、注销状态不用改表结构。card_no 是业务上的唯一标识必须加 UNIQUE。CREATE TABLE borrow ( borrow_id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 借阅ID, reader_id INT UNSIGNED NOT NULL COMMENT 读者ID, book_id INT UNSIGNED NOT NULL COMMENT 图书ID, borrow_date DATE NOT NULL COMMENT 借书日期, due_date DATE NOT NULL COMMENT 应还日期, return_date DATE DEFAULT NULL COMMENT 实际还书日期, status TINYINT NOT NULL DEFAULT 0 COMMENT 0在借 1已还 2逾期, PRIMARY KEY (borrow_id), KEY idx_reader (reader_id), KEY idx_book (book_id), KEY idx_status (status), CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader(reader_id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book(book_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT借阅流水表; CREATE TABLE fine ( fine_id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 罚款ID, borrow_id INT UNSIGNED NOT NULL COMMENT 关联借阅ID, reader_id INT UNSIGNED NOT NULL COMMENT 读者ID, overdue_days INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 逾期天数, amount DECIMAL(6,2) NOT NULL DEFAULT 0.00 COMMENT 罚款金额, paid TINYINT NOT NULL DEFAULT 0 COMMENT 0未缴 1已缴, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 生成时间, PRIMARY KEY (fine_id), KEY idx_reader (reader_id), KEY idx_borrow (borrow_id), CONSTRAINT fk_fine_borrow FOREIGN KEY (borrow_id) REFERENCES borrow(borrow_id), CONSTRAINT fk_fine_reader FOREIGN KEY (reader_id) REFERENCES reader(reader_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT罚款表;borrow 表是整个系统的核心流水。due_date 用 DATE 而不是 DATETIME因为借阅业务只关心到天DATETIME 反而会在查询和格式化时多一步处理。status 用 0/1/2 表示在借、已还、逾期未还逾期状态其实可以由 due_date 和 return_date 推导但单独维护一个状态字段能让查询语句简单很多。两张表都声明了外键MySQL 会保证 borrow 里不会出现不存在的 reader_id 或 book_id。fine 表里的 overdue_days 和 amount 是我刻意留下的冗余。罚款金额虽然可以通过逾期天数 × 单价现算但罚款规则一旦调整历史罚款记录就会失真所以在还书结算那一刻就把天数和金额固化成快照。这也是答辩里一个很好的加分点你能说清楚每个冗余字段存在的原因。2.3 初始化数据测试读者与样例图书初始化数据是课设里最容易偷懒、但答辩必被问的部分。我的习惯是至少准备三个读者、五本图书覆盖正常、冻结、额度用完三种状态这样演示借书流程时每个分支都能点到。注意不要手动指定主键让自增生成否则插入顺序一乱外键关系就跟着乱。INSERT INTO reader(card_no, name, phone, max_borrow, borrowed_count, status) VALUES (R2024001, 张三, 13800000001, 5, 0, 1), (R2024002, 李四, 13800000002, 5, 2, 1), (R2024003, 王五, 13800000003, 5, 3, 0); INSERT INTO book(isbn, title, author, category, total_count, available_count, price) VALUES (9787111213826, 深入理解计算机系统, Randal E.Bryant, 计算机, 3, 3, 99.00), (9787115428028, 数据库系统概论, 王珊, 计算机, 2, 2, 45.00), (9787020002207, 红楼梦, 曹雪芹, 文学, 4, 4, 59.70), (9787302518367, MySQL技术内幕, 姜承尧, 计算机, 2, 2, 89.00), (9787544270878, 三体, 刘慈欣, 科幻, 5, 5, 35.00);第三个读者王五的 status 是 0第一个读者张三的 borrowed_count 是 0这就是给后续演示准备的边界数据借书时分别验证冻结拒绝和正常借出两条路径。2.4 索引策略哪些列建索引、哪些列别建索引不是越多越好这是课设里最常见的误用。我在这套表里只建了四类索引主键索引、唯一索引、外键列索引、等值查询索引。外键列必须建索引——InnoDB 在删除父表行时要检查子表是否存在引用没有索引的话这个检查就是全表扫描而且按 reader_id 查某个读者的借阅历史是高频查询索引同时服务了约束和查询。status 这类低基数列我反而没建索引字段只有 0/1/2 三个取值选择性太差索引扫描和全表扫描区别不大还多占空间。注意建表时只给 borrow 的 reader_id、book_id 建了单列索引没有建(reader_id, book_id)联合索引。如果后续要查某读者是否借过某本书可以再加联合索引但要靠 EXPLAIN 验证实际收益不要想当然地加。3. 借还书流程存储过程、行锁与事务边界3.1 借书存储过程库存校验与 FOR UPDATE 行锁借书不是一条 INSERT 能搞定的。它涉及三个动作扣减图书可借数量、增加读者的在借册数、插入一条借阅记录。三个动作要么全成功要么全不做这就是事务的用武之地。我用一个存储过程把整段逻辑包起来业务逻辑收口在数据库层Java 端只需要一次 CALL。DELIMITER // CREATE PROCEDURE sp_borrow_book( IN p_reader_id INT UNSIGNED, IN p_book_id INT UNSIGNED, IN p_days TINYINT UNSIGNED, OUT p_result TINYINT ) BEGIN DECLARE v_avail INT DEFAULT 0; DECLARE v_borrowed INT DEFAULT 0; DECLARE v_max INT DEFAULT 5; DECLARE v_status INT DEFAULT 0; START TRANSACTION; SELECT available_count INTO v_avail FROM book WHERE book_id p_book_id FOR UPDATE; SELECT status, borrowed_count, max_borrow INTO v_status, v_borrowed, v_max FROM reader WHERE reader_id p_reader_id FOR UPDATE; IF v_status 0 THEN SET p_result -3; -- 读者被冻结 ROLLBACK; ELSEIF v_avail 1 THEN SET p_result -1; -- 库存不足 ROLLBACK; ELSEIF v_borrowed v_max THEN SET p_result -2; -- 超出可借册数 ROLLBACK; ELSE UPDATE book SET available_count available_count - 1 WHERE book_id p_book_id; UPDATE reader SET borrowed_count borrowed_count 1 WHERE reader_id p_reader_id; INSERT INTO borrow(reader_id, book_id, borrow_date, due_date, status) VALUES(p_reader_id, p_book_id, CURDATE(), DATE_ADD(CURDATE(), INTERVAL p_days DAY), 0); SET p_result 0; COMMIT; END IF; END// DELIMITER ;重点说两处。第一两个 SELECT 都加了 FOR UPDATE这是并发安全的关键——不加锁时两个连接同时读到 available_count1各自都认为能借扣两次就把库存扣成负数加了行锁第二个连接必须等第一个提交后才能读到新值。第二p_result 这个 OUT 参数是给上层判断结果用的我约定的编码是 0 成功、-1 库存不足、-2 超出可借额度、-3 账号冻结Java 端拿到非 0 直接弹对应提示。提示FOR UPDATE 必须在事务里才有意义这也是为什么存储过程里显式写了 START TRANSACTION 和 COMMIT/ROLLBACK。如果拆成多条 SQL 在应用层执行锁的边界很容易失控。调用方式很简单p_days 传借阅天数业务上默认 30 天CALL sp_borrow_book(1, 1, 30, r); SELECT r; -- 0 表示借出成功-1/-2/-3 分别是三种拒绝分支3.2 还书存储过程DATEDIFF 算逾期与罚款落表还书的动作和借书对称更新借阅记录状态、回补图书库存、减少读者在借数量、检查是否逾期并生成罚款。DELIMITER // CREATE PROCEDURE sp_return_book( IN p_borrow_id INT UNSIGNED, OUT p_fine DECIMAL(6,2) ) BEGIN DECLARE v_reader_id INT UNSIGNED; DECLARE v_book_id INT UNSIGNED; DECLARE v_due_date DATE; DECLARE v_status TINYINT DEFAULT 0; DECLARE v_overdue INT DEFAULT 0; START TRANSACTION; SELECT reader_id, book_id, due_date, status INTO v_reader_id, v_book_id, v_due_date, v_status FROM borrow WHERE borrow_id p_borrow_id FOR UPDATE; IF v_status 1 THEN SET p_fine 0; -- 已经还过直接退出 ROLLBACK; ELSE UPDATE borrow SET return_date CURDATE(), status 1 WHERE borrow_id p_borrow_id; UPDATE book SET available_count available_count 1 WHERE book_id v_book_id; UPDATE reader SET borrowed_count borrowed_count - 1 WHERE reader_id v_reader_id; SET v_overdue DATEDIFF(CURDATE(), v_due_date); IF v_overdue 0 THEN SET p_fine v_overdue * 0.50; -- 每天 0.5 元可自行调整 INSERT INTO fine(borrow_id, reader_id, overdue_days, amount, paid) VALUES(p_borrow_id, v_reader_id, v_overdue, p_fine, 0); END IF; COMMIT; END IF; END// DELIMITER ;逾期天数用 DATEDIFF 计算这里有个写反参数的血泪经验DATEDIFF(结束日期, 开始日期)结果是前者减后者。还书场景里就是DATEDIFF(CURDATE(), v_due_date)今天减去应还日期正数是逾期天数负数说明提前还了罚款为 0。这个存储过程同时展示了罚款的结算逻辑金额在还书那一刻算好并 INSERT 进 fine 表paid默认 0未缴后续做缴纳功能时 UPDATE 成 1 即可。课设演示时可以把逾期读者单独列一个待缴罚款清单闭环就完整了。3.3 隔离级别选择为什么默认 REPEATABLE READ 够用MySQL InnoDB 默认隔离级别是 REPEATABLE READ课设阶段不需要去改全局配置。很多人觉得不可重复读听起来不安全就想当然升到 SERIALIZABLE结果并发度直线下降。实际上在借书场景里真正的并发风险是两个事务同时读到 available_count1 然后都去扣减这个问题单靠隔离级别解决不了——RR 下的普通 SELECT 是非锁定读不会挡住别的事务修改同一行。正确做法就是 3.1 里的 SELECT ... FOR UPDATE显式加行锁让第二个事务阻塞在读取阶段。隔离级别脏读不可重复读幻读借书场景是否够用READ UNCOMMITTED可能可能可能否READ COMMITTED否可能可能够用需配 FOR UPDATEREPEATABLE READ否否否MVCC够用MySQL 默认SERIALIZABLE否否否可用但并发差这张表是答辩时可以直接背出来的。核心论点就一句REPEATABLE READ 配合 FOR UPDATE 行锁已经能覆盖借书场景的全部并发问题课设不需要更高级的隔离级别。3.4 触发器审计日志借还动作自动留痕借还记录的谁在什么时间借了什么书属于典型的审计需求用触发器做最省事——它不依赖应用层每次记得写日志代码只要对 borrow 表 INSERT 和 UPDATE日志自动落表。CREATE TABLE borrow_log ( log_id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 日志ID, borrow_id INT UNSIGNED NOT NULL COMMENT 借阅ID, action VARCHAR(10) NOT NULL COMMENT BORROW 或 RETURN, reader_id INT UNSIGNED NOT NULL COMMENT 读者ID, book_id INT UNSIGNED NOT NULL COMMENT 图书ID, op_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 操作时间, PRIMARY KEY (log_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT借阅审计日志; DELIMITER // CREATE TRIGGER trg_borrow_log AFTER INSERT ON borrow FOR EACH ROW BEGIN INSERT INTO borrow_log(borrow_id, action, reader_id, book_id) VALUES (NEW.borrow_id, BORROW, NEW.reader_id, NEW.book_id); END// CREATE TRIGGER trg_return_log AFTER UPDATE ON borrow FOR EACH ROW BEGIN IF NEW.status 1 AND OLD.status 0 THEN INSERT INTO borrow_log(borrow_id, action, reader_id, book_id) VALUES (NEW.borrow_id, RETURN, NEW.reader_id, NEW.book_id); END IF; END// DELIMITER ;AFTER INSERT 触发器里 NEW 代表刚插入的行所以直接取 NEW.borrow_id 写日志。AFTER UPDATE 触发器加了NEW.status 1 AND OLD.status 0的判断避免每次 UPDATE 都记一条——只有状态从在借变成已还才记录 RETURN。不过触发器有个要留意的边界它把业务逻辑藏进了数据库排错时第一反应是看应用代码容易漏掉触发器这一层。我的习惯是触发器只做审计这种旁路动作核心的扣库存、算罚款都放在存储过程里显式执行这样逻辑链路是看得见的。4. 查询与增删改查视图封装、统计 SQL 与 JDBC 连接4.1 高频统计查询借阅排行榜、逾期名单、在借清单数据库课设的增删改查不能只有单表 CRUD多表联查和统计聚合才是拿分点。下面三个查询是这个系统里最高频的直接抄-- 最近30天借阅排行榜 SELECT bk.title, COUNT(*) AS borrow_times FROM borrow b INNER JOIN book bk ON b.book_id bk.book_id WHERE b.borrow_date DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY bk.book_id, bk.title ORDER BY borrow_times DESC LIMIT 10; -- 逾期未还名单 SELECT r.card_no, r.name, bk.title, b.due_date, DATEDIFF(CURDATE(), b.due_date) AS overdue_days FROM borrow b INNER JOIN reader r ON b.reader_id r.reader_id INNER JOIN book bk ON b.book_id bk.book_id WHERE b.status 0 AND b.due_date CURDATE(); -- 某读者的在借清单 SELECT bk.title, bk.isbn, b.borrow_date, b.due_date FROM borrow b INNER JOIN book bk ON b.book_id bk.book_id WHERE b.reader_id 1 AND b.status 0;排行榜查询有三个细节。GROUP BY 不能只写 bk.book_id 而 SELECT 里带 bk.title严格模式下会报错规范写法是把两个都放进 GROUP BYDATE_SUB(CURDATE(), INTERVAL 30 DAY) 是当前日期往前推 30 天比先算日期再拼字符串可靠LIMIT 10 控制了返回行数数据量大时避免结果集过大。逾期名单的查询条件是status 0 AND due_date CURDATE()这就是 2.2 里保留 status 字段的价值——如果全靠日期推导这条 SQL 会复杂不少。索引方面idx_reader 和 idx_status 会分别服务按读者查流水和按状态查逾期两个方向。4.2 视图封装三表连接收敛成一张逻辑表借阅详情在界面里几乎每个页面都要用每次写三表 JOIN 太啰嗦而且应用层能看到全部字段也不是好事。我建了一个视图把连接逻辑收口CREATE VIEW v_borrow_detail AS SELECT b.borrow_id, r.card_no, r.name AS reader_name, bk.title, bk.isbn, bk.category, b.borrow_date, b.due_date, b.return_date, b.status FROM borrow b INNER JOIN reader r ON b.reader_id r.reader_id INNER JOIN book bk ON b.book_id bk.book_id;视图建好后界面的借阅记录页面只需要一句SELECT * FROM v_borrow_detail WHERE status 0 ORDER BY borrow_date DESC不用关心底层表结构。要注意视图不是性能优化手段它只是把 JOIN 逻辑封装起来了底层照样是三条连接课设里视图的定位是简化查询和隐藏表结构不是加速。4.3 JDBC 连接池HikariCP 最小配置与参数含义应用层连 MySQL 不能每次查询都新建 Connection那是课设里最常见的性能问题。连接池是标准答案Spring Boot 默认带 HikariCP单独写课设时手动配一下也很简单HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/library_db ?useUnicodetruecharacterEncodingutf8mb4 serverTimezoneAsia/ShanghaiuseSSLfalse); config.setUsername(root); config.setPassword(123456); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(3000); config.setPoolName(libraryPool); HikariDataSource dataSource new HikariDataSource(config);参数取值含义maximumPoolSize10连接池最大连接数课设 5-10 足够minimumIdle2空闲保底连接避免频繁新建连接connectionTimeout3000获取连接超时时间超过直接报错而不是无限等characterEncodingutf8mb4与表字符集保持一致乱码根因之一连接串里serverTimezoneAsia/Shanghai是给 MySQL 8 驱动的不写会报时区错误useSSLfalse是本地开发环境关掉 SSL省掉证书配置的麻烦。用 Druid 或 C3P0 也完全可以核心是连接复用不是具体哪家实现。4.4 增删改查封装PreparedStatement 参数化防注入DAO 层我始终坚持用 PreparedStatement一个分页查询的典型写法是public ListBook searchBook(String keyword, int page, int pageSize) { String sql SELECT book_id, isbn, title, author, category, total_count, available_count FROM book WHERE title LIKE ? OR isbn LIKE ? ORDER BY book_id DESC LIMIT ?, ?; ListBook list new ArrayList(); try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { String like % keyword %; ps.setString(1, like); ps.setString(2, like); ps.setInt(3, (page - 1) * pageSize); ps.setInt(4, pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Book b new Book(); b.setBookId(rs.getInt(book_id)); b.setIsbn(rs.getString(isbn)); b.setTitle(rs.getString(title)); b.setAuthor(rs.getString(author)); b.setAvailableCount(rs.getInt(available_count)); list.add(b); } } } catch (SQLException e) { logger.error(searchBook failed, keyword{}, keyword, e); } return list; }四个参数占位符的含义两个 LIKE 参数值都是%关键字%实现模糊匹配LIMIT ?, ?是分页第一个参数是偏移量(page-1)*pageSize第二个是每页行数。try-with-resources 保证 Connection、PreparedStatement、ResultSet 都自动关闭不会把连接池的连接漏掉。为什么不用字符串拼接因为拼接出来的 SQL 里用户输入会被当成 SQL 代码执行典型的注入name a OR 11能把整张表带出来。PreparedStatement 的参数由驱动转义输入永远只是数据不是 SQL这是红线级别的习惯。5. 避坑与常见问题答辩前必须排掉的五个雷这五个问题是我做课设辅导时被问得最多的也是我自己当初一个个踩出来的。每条都按现象 → 原因 → 解决整理答辩前对着过一遍能少挨不少问。5.1 外键删除被拒现象执行DELETE FROM reader WHERE reader_id 2报错提示Cannot delete or update a parent row: a foreign key constraint fails。原因reader_id2 的读者在 borrow 表里存在借阅记录外键约束不允许删除被引用的父表行这是数据库在保护数据完整性。解决删除前先查引用再处理SELECT COUNT(*) FROM borrow WHERE reader_id 2;如果有在借记录先走还书流程如果是历史记录且确实要删读者业界标准做法是软删除——把 reader 的 status 改成 3注销而不是物理删行。答读者注销怎么办时标准答案就是软删除。5.2 并发借书库存变负数现象两个管理窗口同时借同一本书演示完发现 available_count 变成 -1。原因UPDATE book SET available_count available_count - 1本质是读旧值 → 计算新值 → 写回三步两个事务交错执行就会丢失更新这是典型的并发场景。解决两种方案看课设要求选。存储过程里 SELECT ... FOR UPDATE 先锁行后面 UPDATE 就安全了更省事的是带条件更新UPDATE book SET available_count available_count - 1 WHERE book_id ? AND available_count 0;然后检查受影响行数为 0 说明没库存。注意后者只能防超卖这一种情况整套借阅流程的原子性还是要靠事务。如果演示时真碰到死锁报错Deadlock found多半是两个事务加锁顺序不一致统一所有事务先锁 book 再锁 reader 就能解决。5.3 中文乱码现象界面输入数据库三个字存进库里变成???。原因三层字符集不一致——连接串的 characterEncoding、表的 DEFAULT CHARSET、客户端工具各自为政MySQL 按其中一层解释中文字节就乱了。解决三层统一成 utf8mb4CREATE DATABASE library_db DEFAULT CHARACTER SET utf8mb4;连接串加characterEncodingutf8mb4工具连接也显式选 utf8mb4。注意是 utf8mb4 不是 utf8MySQL 的 utf8 是残缺的 utf8mb3遇到 emoji 或生僻字照样翻车。5.4 备份恢复翻车现象答辩前用 mysqldump 备份恢复时报错或者恢复后数据少一段。原因两个经典错误。备份没加--single-transaction备份期间业务还在写逻辑备份的数据在表之间对不上恢复时没先建库直接往不存在的库导入。解决mysqldump -uroot -p --single-transaction --default-character-setutf8mb4 library_db library_backup.sql mysql -uroot -p -e CREATE DATABASE IF NOT EXISTS library_db DEFAULT CHARACTER SET utf8mb4 mysql -uroot -p library_db library_backup.sql--single-transaction利用 InnoDB 的 MVCC 做一致性快照不加的话每张表独立备份表间数据逻辑上不一致--default-character-set不加的话备份文件里中文注释恢复时可能乱码。5.5 索引失效现象查询条件里明明有索引列EXPLAIN 却显示 typeALL 全表扫描。原因三种常见情况。索引列上套了函数比如WHERE YEAR(create_time) 2024LIKE 用了前导通配符%数据库%字符串列被拿数字去查触发隐式类型转换。解决EXPLAIN 看 type 字段从 ALL 变成 ref 才算生效。前导通配符改成后缀匹配数据库%或者用全文索引列上套函数改成范围查询WHERE create_time BETWEEN 2024-01-01 AND 2024-12-31;这条在答辩时几乎必问能说出函数包住索引列会导致索引失效这句话就比大多数人强了。6. 验证与答辩EXPLAIN、十万行压测与演示路径6.1 用 EXPLAIN 把索引生效讲给答辩老师听建完表、写完流程最后一步是证明你的设计是稳的。我最喜欢用的手段是 EXPLAIN——它输出一行表直接把优化器的执行计划摊开EXPLAIN SELECT b.borrow_id, r.name, bk.title FROM borrow b INNER JOIN reader r ON b.reader_id r.reader_id INNER JOIN book bk ON b.book_id bk.book_id WHERE b.status 0 AND b.reader_id 1;看三列就够。type 列从 ALL 到 ref 再到 eq_ref 是递进的key 列显示用到了哪个索引名比如 idx_readerrows 列是优化器估算扫描的行数。答辩时这么讲这个查询驱动表 borrow 走了 idx_reader 索引扫描 2 行然后通过主键 eq_ref 回查 reader 和 book总共扫描 4 行而不是全表扫几万行。有数字有索引名比空口说我建了索引有说服力得多。6.2 十万行压测与演示路径设计光有索引还不够数据量一上来才能看出差距。我给课设准备了一个造数存储过程DELIMITER // CREATE PROCEDURE sp_gen_books(IN p_count INT) BEGIN DECLARE i INT DEFAULT 1; WHILE i p_count DO INSERT INTO book(isbn, title, author, category, total_count, available_count) VALUES ( CONCAT(978, LPAD(FLOOR(RAND() * 9999999999), 10, 0)), CONCAT(压测图书, i), CONCAT(作者, i % 500), ELT(1 FLOOR(RAND() * 5), 计算机, 文学, 科幻, 历史, 经济), 10, 10 ); SET i i 1; END WHILE; END// DELIMITER ; CALL sp_gen_books(100000);RAND 生成随机数LPAD 补零凑 ISBNELT 从列表里随机取分类。笔记本上插十万行大约一两分钟嫌慢就改成五万。造完数据再跑 6.1 的 EXPLAIN把扫描行数对比给老师看如果某个查询真的慢了开慢查询日志看现场SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; -- 超过1秒的记录 SHOW VARIABLES LIKE slow_query_log_file;演示路径我建议固定成一条闭环登录 → 新增一个读者 → 借书先演示正常分支再演示冻结拒绝分支→ 还书 → 查罚款 → 缴纳 → 看排行榜。每一步都对应一个存储过程或一条查询走到罚款那步时顺手把 borrow 表的状态和 fine 表的数据一起展示证明事务性。收尾把 EXPLAIN 结果摆出来整套演示不超过十分钟但覆盖了建表、约束、事务、索引、统计全部知识点。资源包里建表脚本、存储过程、触发器、初始化数据和 Java 源码都按目录分好了照着顺序跑就能复现整套流程。从那以后我每次做课设项目都会强制自己走一遍约束检查 十万行压测 边界数据演示这三件事能把这套流程完整走完的项目答辩基本不会被问倒。希望帮到你。本文还有配套的精品资源点击获取
返回列表