资讯动态

图书借阅管理系统课设实战:从ER图到MySQL存储过程与并发控制

发布时间:2026/10/14 11:21:14 来源:尧图企业网站定制
简介一份数据库系统课程设计报告以图书借阅管理系统为主题面向需要完成数据库课程设计或复习数据库设计全流程的计算机专业学生。报告以MySQL为实现环境围绕图书借阅业务场景完整覆盖系统需求分析、数据流/业务流分析、数据字典、概念结构设计、逻辑结构设计、物理实现与功能调试等环节。概念设计中包含实体、属性、联系分析及PDM图逻辑设计中说明一对一、一对多、多对多关系转换物理实现部分给出表设计、创建表与完整性约束代码以及视图、索引、存储过程和触发器的创建方法。功能调试部分则演示借阅者查询书籍、管理员对图书信息的增删改查等典型模块便于对照实践。资源为单个docx文档共1个文件压缩包约575KB目录层次清晰适合边读边做。目前已有53人学习下载对正在展开数据库课程设计的学生有直接参考价值。1. 图书借阅管理系统是什么一个数据库课设为何值得认真做数据库系统课程设计报告里的“图书借阅管理系统”是《数据库系统概论》《数据库系统原理》这类课程里最经典的综合训练题目它要求你从需求分析出发完成ER图设计、关系模式转换、建库建表、数据操作再到系统验证的完整闭环。很多同学把它当成“写一个借书还书的增删改查”结果答辩时被一问“为什么这里要设外键”“并发下这本书会被借爆吗”就翻车。实际上这个题目麻雀虽小五脏俱全恰好覆盖了关系模型、完整性约束、事务隔离、存储过程与权限控制这些数据库系统最核心的知识点。适合正在做课设的学生也适合想系统补一遍数据库落地能力的从业者。下面的内容完全按照我实际完成的方案讲清楚怎么设计模型、怎么写SQL、坑在哪里、报告要怎么写才能让老师相信你真做过了。2. 从ER图到关系模式把借阅业务拆成表和主键2.1 实体与属性怎么定避免过度设计拿到图书借阅管理系统先不要急着写SQL第一步永远是画ER图。常见做法是定义读者、图书、借阅记录三个核心实体。读者有读者号、姓名、类型学生/教师、办证日期图书有图书号、书名、作者、出版社、ISBN、可借数量借阅记录是读者和图书之间的联系带有借书日期和应还日期。属性设计有三个容易被老师追问的点。第一不要把“可借数量”设计成“库存总量”。图书管理系统的库存总量是馆藏量可借数量是未被借出的副本数二者在还书操作时更新逻辑完全不同。第二读者身份信息中手机号和邮箱这类扩展属性可以用单独的联系方式表也可以直接放在读者表课程设计阶段直接放读者表即可不要为了范式而拆出一堆没必要的表。第三ISBN和图书号必须区分开ISBN是书的国际标准编号同一种书有多本副本时ISBN相同但每本书的物理副本应该有独立的图书号。2.2 联系的主键设计借阅关系该拿什么做主键这是整个设计中最容易出问题的地方。读者和图书之间是多对多联系借阅记录表的主键我见过很多方案用“读者号图书号”联合做主键这在逻辑上是错的同一个读者完全可以分两次借两本不同的书甚至在还书后又借同一本书如果主键是读者号, 图书号第二次借阅就会被主键冲突挡住。正确做法是给借阅记录一个自增的借阅流水号作为代理主键读者号和图书号只作为外键。这样做还有一个好处一本书同时被借出多本副本时同一读者号同一图书号的组合不会因为主键约束而阻塞。借阅状态字段建议用枚举类型或TINYINT我一般用TINYINT0表示借出未还1表示已归还。应还日期在记录插入时根据借阅规则计算例如学生30天、教师60天这个规则可以用借阅类型表或者直接写在存储过程中。提示不要用读者号图书号借出日期做联合主键。虽然能区分两次借阅但外键关联和索引维护都会变得复杂课程设计答辩时还容易被追问“你考虑过同一天借两次相同的书吗”。2.3 结合范式检查把冗余消灭在建模阶段关系模式设计完成后逐个做范式检查。借阅记录表里不应出现“图书名称”、“读者姓名”这类冗余派生字段需要书名时通过JOIN图书表获取这是第三范式的基本要求。《数据库系统概念》第七版里专门讲了范式理论课设实战中不需要做到BCNF但至少要保证没有部分依赖和传递依赖。有一个反范式设计的例外如果你需要频繁查询借阅历史并展示书名可以在借阅记录表中冗余一个“借出时书名快照”字段。这是因为图书信息可能被删除或修改而借阅历史应该保留当时的信息。这个设计在答辩时可以主动提出来说明你理解范式理论也理解实际业务需要权衡。我当时就是这么做的老师反而认可这种有意识的取舍。图书分类这个属性如果没有单独的复杂层次结构直接放在图书表中作为普通字段即可不要为了“看起来规范”而拆出分类表。3. 建库建表完整SQL在MySQL 8.0上跑通最小闭环3.1 建库与存储引擎选择数据库引擎优先选InnoDB。在MySQL 8.0中InnoDB是默认引擎支持事务、外键约束和行级锁这三个能力在当前项目里全都是必需品。MyISAM虽然查询快但不支持事务和外键一旦并发借书就会出现数据不一致千万不要图省事用它。建库语句我一般显式声明字符集和排序规则。CREATE DATABASE IF NOT EXISTS library_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_0900_ai_ci; USE library_db;字符集必须用utf8mb4而不是utf8。MySQL旧版本中的utf8字符集最多存储3字节像生僻字和部分扩展符号会报错utf8mb4是完整的4字节UTF-8编码。排序规则用utf8mb4_0900_ai_ci这是MySQL 8.0默认的支持不区分大小写的拼音排序。如果你用的是MySQL 5.7则改写成utf8mb4_general_ci语法向下兼容性好。3.2 建表语句与约束设计核心三张表的建表SQL如下。读者表设置主键、唯一约束和非空约束图书表设置默认值和检查约束借阅记录表设置外键和索引。CREATE TABLE reader ( reader_id INT AUTO_INCREMENT PRIMARY KEY, reader_no VARCHAR(20) NOT NULL UNIQUE, reader_name VARCHAR(50) NOT NULL, reader_type TINYINT NOT NULL DEFAULT 1 COMMENT 1-学生 2-教师, max_borrow_num INT NOT NULL DEFAULT 5, created_date DATE NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-正常 0-禁用 ) ENGINEInnoDB; CREATE TABLE book ( book_id INT AUTO_INCREMENT PRIMARY KEY, book_no VARCHAR(20) NOT NULL UNIQUE, book_name VARCHAR(200) NOT NULL, author VARCHAR(100), publisher VARCHAR(100), isbn VARCHAR(20), category VARCHAR(50), total_count INT NOT NULL DEFAULT 1, available_count INT NOT NULL DEFAULT 1, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-在架 0-下架, CONSTRAINT chk_count CHECK (total_count 0 AND available_count 0) ) ENGINEInnoDB; CREATE TABLE borrow_record ( borrow_id INT AUTO_INCREMENT PRIMARY KEY, reader_id INT NOT NULL, book_id INT NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE NULL, renew_count TINYINT NOT NULL DEFAULT 0, fine_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-借出 1-已还, CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader(reader_id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book(book_id), INDEX idx_borrow_reader (reader_id), INDEX idx_borrow_status (status) ) ENGINEInnoDB;三条建表语句中外键约束直接声明在表内创建顺序必须是先建被引用表reader、book再建引用表borrow_record否则MySQL会报外键关联的表不存在。available_count字段必须加上CHECK约束检查非负否则并发场景下可借数量可能变成负数。索引方面除了主键外borrow_record 表上的外键字段要单独建索引。InnoDB虽然会自动为外键列建索引但显式声明可以让执行计划更稳定。3.3 必调参数与常见翻车点从MySQL 5.7迁移到8.0时容易翻车。第一个坑是认证插件MySQL 8.0默认caching_sha2_password老的JDBC驱动只支持mysql_native_password连不上时错误信息会提示“Unable to load authentication plugin”。对应做法是建用户时指定插件或者升级驱动。第二个坑是SQL语法更严格GROUP BY不再隐式支持未聚合字段第6章的统计查询如果涉及非聚合列就写不通过必须在SELECT里用ANY_VALUE或正确分组。第三个坑是datetime默认值MySQL 5.7允许DEFAULT CURRENT_TIMESTAMP只用于TIMESTAMP类型8.0中也允许用于DATETIME但写法必须是DEFAULT CURRENT_TIMESTAMP不能加函数括号注意区分CURRENT_TIMESTAMP和NOW()在这里的语法兼容性。初始化基础数据时建议用事务包裹插入。如果某一条数据失败整体回滚避免主从数据不一致START TRANSACTION; INSERT INTO reader (reader_no, reader_name, reader_type, created_date) VALUES (S001, 张明, 1, 2024-03-01), (T001, 李华, 2, 2024-03-01); INSERT INTO book (book_no, book_name, author, publisher, isbn, total_count, available_count) VALUES (B001, 数据库系统概论第6版, 王珊, 高等教育出版社, 9787040401765, 3, 3); COMMIT;4. 借书还书事务存储过程与并发控制实战4.1 事务边界借书根本不是两条UPDATE借书流程看起来简单查一下书是否可借 → 插入借阅记录 → 可借数量减1。但在多终端并发场景下三个步骤如果分开执行就会有两个读者同时拿到“可借数量为1”的结果最后把书借超。引入事务后这个逻辑依然有漏洞如果事务隔离级别是默认的REPEATABLE READ查询时使用快照读两条并发事务会同时看到可借数量为1。正确做法是在事务内对图书行加锁用SELECT ... FOR UPDATE做当前读让第二个事务阻塞到第一个提交后才继续执行。这也是把“查询更新”拆成“加锁查询更新插入”的事务边界锁一定要在第一步拿到而不是先SELECT判断再UPDATE因为先判断再更新在并发下会产生竞态条件这就是老师最喜欢深挖的“你如何保证一本书不会被两个人同时借走”。4.2 借书存储过程的完整实现存储过程把借书逻辑封装在MySQL服务端客户端只需要调用一个CALL事务、加锁、回滚全部在服务端完成。这既保证了原子性也让权限管理更简单——应用层不直接授权DML只授权调用存储过程。DELIMITER // CREATE PROCEDURE sp_borrow_book( IN p_reader_no VARCHAR(20), IN p_book_no VARCHAR(20) ) BEGIN DECLARE v_reader_id INT; DECLARE v_book_id INT; DECLARE v_available INT; DECLARE v_borrowed_count INT; DECLARE v_max_num INT; START TRANSACTION; SELECT reader_id, max_borrow_num INTO v_reader_id, v_max_num FROM reader WHERE reader_no p_reader_no AND status 1 FOR UPDATE; IF v_reader_id IS NULL THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 读者不存在或已禁用; END IF; SELECT COUNT(*) INTO v_borrowed_count FROM borrow_record WHERE reader_id v_reader_id AND status 0; IF v_borrowed_count v_max_num THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 该读者借书数量已达上限; END IF; SELECT book_id, available_count INTO v_book_id, v_available FROM book WHERE book_no p_book_no AND status 1 FOR UPDATE; IF v_book_id IS NULL THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 图书不存在或已下架; END IF; IF v_available 0 THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 图书全部借出; END IF; INSERT INTO borrow_record (reader_id, book_id, borrow_date, due_date, status) VALUES (v_reader_id, v_book_id, CURDATE(), DATE_ADD(CURDATE(), INTERVAL 30 DAY), 0); UPDATE book SET available_count available_count - 1 WHERE book_id v_book_id; COMMIT; END // DELIMITER ;这个存储过程的几个关键细节。SELECT ... INTO ... FOR UPDATE是对查询结果加排他锁既拿到了读者或图书的行锁又把数据存入变量一举两得。加锁顺序必须是先读者后图书全项目统一这个顺序可以避免死锁。SIGNAL SQLSTATE 45000是MySQL的主动报错方式回滚后抛出业务异常客户端能捕获到具体错误信息。应还日期通过DATE_ADD计算这里固定按30天实际系统可以从读者类型表读取借阅天数规则。调用端代码只需要写CALL sp_borrow_book(S001, B001);4.3 还书流程与逾期罚金计算还书逻辑比借书多一个逾期判断。先取借阅记录判断是否已归还如果已归还就直接抛错防止重复还书。接着计算逾期天数DATEDIFF(CURDATE(), due_date)大于0则有罚金罚金按每天0.10元计算保留两位小数。最后把记录状态改为已还、登记实际还书日期、更新罚金、图书可借数量加1。DELIMITER // CREATE PROCEDURE sp_return_book( IN p_borrow_id INT ) BEGIN DECLARE v_status TINYINT; DECLARE v_due_date DATE; DECLARE v_book_id INT; DECLARE v_days_overdue INT; START TRANSACTION; SELECT status, due_date, book_id INTO v_status, v_due_date, v_book_id FROM borrow_record WHERE borrow_id p_borrow_id FOR UPDATE; IF v_status IS NULL THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 借阅记录不存在; END IF; IF v_status 1 THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 该记录已归还请勿重复还书; END IF; SET v_days_overdue DATEDIFF(CURDATE(), v_due_date); IF v_days_overdue 0 THEN UPDATE borrow_record SET return_date CURDATE(), status 1, fine_amount CAST(v_days_overdue * 0.10 AS DECIMAL(10,2)) WHERE borrow_id p_borrow_id; ELSE UPDATE borrow_record SET return_date CURDATE(), status 1 WHERE borrow_id p_borrow_id; END IF; UPDATE book SET available_count available_count 1 WHERE book_id v_book_id; COMMIT; END // DELIMITER ;罚金计算用CAST做类型转换避免DECIMAL和浮点数相乘出现精度漂移。课程设计阶段罚金可以只累计不实际扣款但字段一定要留出来。逾期天数口径上要明确应还日期当天归还不算逾期逾期从次日记起。这里用DATEDIFF(CURDATE(), due_date)计算如果今天正好是应还日差值为0不产生罚金。5. 借阅管理系统实践中的高频避坑四个必查清单5.1 外键约束导致的插入失败现象程序里插入借阅记录时报“Cannot add or update a child row: a foreign key constraint fails”。原因borrow_record表里的reader_id或book_id引用了reader表或book表中不存在的主键。常见原因是应用层在事务提交之前先拿自增ID但ID并未落库或者是删除了读者/图书数据历史借阅记录仍然引用着它们。解决在应用层插入之前先显式检查外键值是否存在或者直接调用存储过程让数据库做校验。如果业务上允许删除读者信息那么读者表只做逻辑删除status置为0不做物理删除这样借阅历史中的外键永远不会指向空值。5.2 并发下同一本书被重复借出现象压测时开两个事务同时借同一本只剩1本可借的书最终可借数量变成-1借阅记录里插入了两条。原因两个事务都先执行普通SELECT判断available_count大于0然后插入记录再UPDATE。REPEATABLE READ下的快照读让两个事务都认为图书是可借的而UPDATE又是第二顺位才执行最终更新覆盖了对方的修改。解决事务一开始就执行SELECT ... FOR UPDATE对图书行加排他锁。第一个事务提交前第二个事务的SELECT FOR UPDATE会阻塞等待提交后重新读到的available_count已经是0于是走“图书全部借出”的分支。这也是在第四章存储过程里把加锁放在最开始的原因。5.3 触发器造成的递归调用和隐式提交现象在borrow_record表上写了AFTER INSERT触发器去更新book表的available_count结果触发器又触发了另一张表的更新甚至出现递归调用报错。另外一个隐蔽问题是借书事务明明回滚了但可借数量却变了。原因MySQL触发器内部如果有UPDATE同一张表可能造成递归调用而存储过程里使用触发器时还要注意某些语句导致隐式提交如DDL一旦触发就会让事务控制失效。更常见的设计问题在于事务里的逻辑放在触发器中执行使得事务边界变得不可见排查困难。解决不要在触发器里做业务数据更新触发器只适合记录日志类操作如插入借阅操作日志到history表。事务内的业务更新全部在存储过程中显式编写。如果要用触发器记录日志也要注意触发器执行失败会导致整个事务回滚因此日志表中不能包含NOT NULL无默认值的字段。5.4 连接数被打满与“too many connections”现象压测或者多人同时在线使用时客户端报“Too many connections”数据库服务看起来没有卡死但就是拒绝连接。原因应用层没有使用连接池每次请求新建一个数据库连接用完又没有及时关闭。遇到借书事务中某个步骤慢连接就被持续占用连接数逼近上限。解决应用层配置HikariCP连接池初始化连接数5最大连接数20连接超时30秒。数据库侧设置max_connections200MySQL默认151但上调前先计算实际内存开销每连接约占用几MB到几十MB过大又会吃满内存。按照课设的并发规模连接池配到最大值20基本够用。另外存储过程内部减少了多次网络往返也变相降低了连接占用时间。5.5 还书时“更新了0行”却没报错现象还书操作返回成功但借阅记录状态没有变化事务还提交了。应用层没判断受影响行数当成成功处理了。原因更新条件里WHERE带上了status0如果记录状态已经是1受影响行数为0但存储过程中没有对ROW_COUNT做判断。更隐蔽的情况是主键传错事务更新了另一条不在预期范围内的记录。解决存储过程在UPDATE之后用ROW_COUNT()检查收益行数如果为0则回滚并SIGNAL业务错误。这个检查原则同样适用于借书流程里的“可借数量减1”如果可借数量因并发原因没有变化同样要回滚。6. 课设报告的验收技巧视图、权限与统计查询课程设计答辩时老师不看你的代码量看的是你对“数据库系统”这个名称的理解。三个点能让你比同组人突出一是提供视图层封装二是做最小权限授权三是写一个有说服力的统计查询。视图的用法是隐藏底层表结构同时简化高频查询。比如展示当前未归还的借阅清单关联读者名和书名应用层直接SELECT * FROM v_current_borrow拿到结果。创建视图和创建权限要分开处理视图所属用户要拥有基表的SELECT权限才能在视图上查询。CREATE VIEW v_current_borrow AS SELECT r.reader_no, r.reader_name, b.book_no, b.book_name, br.borrow_date, br.due_date, DATEDIFF(CURDATE(), br.due_date) AS overdue_days FROM borrow_record br JOIN reader r ON br.reader_id r.reader_id JOIN book b ON br.book_id b.book_id WHERE br.status 0; CREATE USER app_userlocalhost IDENTIFIED BY App123456; GRANT SELECT, INSERT, UPDATE ON library_db.* TO app_userlocalhost; GRANT EXECUTE ON PROCEDURE library_db.sp_borrow_book TO app_userlocalhost; GRANT EXECUTE ON PROCEDURE library_db.sp_return_book TO app_userlocalhost;权限设计上应用账号只授予DML和存储过程执行权不授权DDL。如果用了视图在视图和底层表上都要分别授权。这样即使应用被注入攻击者也拿不到建表、删表的权限这个点写进报告里是加分项。统计查询要体现你懂得分组聚合和业务口径一个直观的例子是月度借阅排行统计每本书的被借次数按借阅次数降序排列。注意MySQL 8.0的ONLY_FULL_GROUP_BY模式要求SELECT列表里出现的非聚合列必须都在GROUP BY中或者用ANY_VALUE包裹写任何分组查询前先考虑这一点。SELECT b.book_no, b.book_name, COUNT(*) AS borrow_times FROM borrow_record br JOIN book b ON br.book_id b.book_id WHERE br.borrow_date BETWEEN 2024-01-01 AND 2024-12-31 GROUP BY b.book_no, b.book_name ORDER BY borrow_times DESC, b.book_no LIMIT 10;我当年这个题目的课设报告里最后放了一段性能验证用脚本并发调用20个线程模拟借书观察可借数量没有被突破上限并把压测前后的数据对比截图放进附录。这个做法在答辩时很加分因为它证明你不是只把SQL跑通了而是真的理解了并发和数据一致性。我的习惯是先做功能再做并发验证最后写报告。报告里每个设计决策都要写明“为什么”而不是罗列表结构。希望今天的这些思路能帮到你让你的图书借阅管理系统不只是一份能交差的课设而是一段真正拿得出手的数据库实践。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑