简介数据库课程设计《图书管理系统》任务书文档围绕数据库系统设计的完整流程展开面向高校软件工程、数据库原理等课程的学生可作为课程设计、期末项目或大作业的参考模板。文档详细安排了从需求分析、概念结构设计、逻辑结构设计、物理结构设计到编码的五个阶段需求分析中明确了学生借阅、续借、归还、查询以及管理员对图书、学生信息和借阅业务的增删改确认等功能概念结构设计要求绘制分E-R图并整合为系统总E-R图逻辑结构设计需要依据转换规则生成关系模式设计主键、外键并做规范化处理物理结构设计则涉及字段存储长度、索引、视图、主码外码定义及建表查询SQL语句。编码部分还提出注释不少于三分之一的规范要求并附带进度安排与推荐参考资料便于读者按步骤完成课程设计报告。整包仅含1个doc文档大小48KB内容精炼集中。已有2737人学习下载适合需要系统梳理图书管理系统数据库设计思路、撰写规范报告的高校学生参考。1. 图书管理系统课程设计一份 .doc 背后的完整数据库落地路径如果你搜到“数据库课程设计--图书管理系统.doc”大概率是正在为这门课找一份能跑、能讲、能答辩的完整方案。图书管理系统是数据库课设里最稳的选题需求一眼能看懂读者、图书、借阅三张核心表刚好覆盖外键约束、事务、索引、并发控制的全部考点又不至于像电商系统那样复杂到失控。这篇笔记不评价那份 .doc 写得好坏只把它拆成一套从建库到初始化数据都能动手复现的实战流程。新手能跟着一步步把系统搭起来熟手也能看到表结构设计里几个容易翻车的边界。2. 需求到表结构图书管理系统的 ER 模型与三张核心表2.1 需求边界你管的是“库存”还是“流通”图书管理系统最常见的翻车原因是拿到题目就开始铺表作者、出版社、ISBN、分类、馆藏位置、书架号最后做出一个“图书信息登记表”真正难的借阅流通反而没做透。我一般先把需求收敛成一句话谁在什么时间借了哪本书该什么时候还逾期了没。围绕这个核心流程建模其余字段都是可以后补的扩展项。从功能上看课程设计至少需要三个模块读者管理、图书管理、借阅管理。读者管理负责维护借阅者信息图书管理负责维护馆藏和库存借阅管理则是把两者用“借还记录”关联起来同时带动库存变化。很多同学把借阅当成简单的两张表关联查询结果漏掉了事务和并发问题答辩时被一句“两个人同时借最后一本书怎么办”问住。这就是需求边界没划清楚真正的系统要解决的是“流通状态的一致性”不是“展示信息”。ER 关系也不复杂一个读者可以借多本图书一本图书可以被多个读者借过所以读者和图书之间是多对多关系。这个多对多关系不能直接交叉相连必须拆成一张借阅表来承载借阅表里存读者 ID、图书 ID、借出日期、应还日期、实际归还日期。这样设计以后要统计逾期、排行、借阅历史都有了数据基础。如果你还想扩展预约、续借、罚款也是在这张借阅表上加状态位而不是另起炉灶。2.2 表结构设计读者表、图书表、借阅表的最小可用定义下面这版 DDL 是我给课程设计用的最小集合字段不多但每一条都能在答辩时说出设计理由。注意建库时就把字符集定成 utf8mb4避免后面中文乱码来回折腾。CREATE DATABASE library DEFAULT CHARSET utf8mb4; USE library; CREATE TABLE reader ( reader_id VARCHAR(20) PRIMARY KEY COMMENT 学号/工号, name VARCHAR(50) NOT NULL COMMENT 姓名, max_borrow TINYINT NOT NULL DEFAULT 5 COMMENT 最大借阅数量, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 建档时间 ) ENGINEInnoDB COMMENT读者表; CREATE TABLE book ( book_id INT AUTO_INCREMENT PRIMARY KEY COMMENT 图书内部ID, title VARCHAR(200) NOT NULL COMMENT 书名, author VARCHAR(100) DEFAULT NULL COMMENT 作者, stock INT NOT NULL DEFAULT 1 COMMENT 馆藏总量, left_stock INT NOT NULL DEFAULT 1 COMMENT 当前可借余量, UNIQUE KEY uniq_title_author (title, author) ) ENGINEInnoDB COMMENT图书表; CREATE TABLE borrow ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 借阅流水ID, reader_id VARCHAR(20) NOT NULL COMMENT 读者ID, book_id INT NOT NULL COMMENT 图书ID, borrow_date DATE NOT NULL COMMENT 借出日期, due_date DATE NOT NULL COMMENT 应还日期, return_date DATE DEFAULT NULL COMMENT 实际归还日期NULL表示未还, UNIQUE KEY uniq_open (reader_id, book_id, return_date), 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 COMMENT借阅记录表;说说几个关键设计。reader_id 用学号/工号做主键而不是自增 ID是因为业务上读者身份唯一且可读字符串主键在千万级数据下性能会吃亏但课程设计这个量级完全没问题还能省一次关联查询。book_id 用自增整数图书可能多次出版同 ISBN所以内部 ID 更稳定题名加作者的唯一键用来防止重复录入同名书。borrow 表主键 id 是流水号同一个读者反复借同一本书会产生多条记录所以不能用 reader_id book_id 做唯一键。有个细节我在很多 .doc 里没见过为什么 borrow 表的唯一键是 (reader_id, book_id, return_date)这是因为 MySQL 的 UNIQUE 索引允许多个 NULL 值一本书借出时 return_date 为 NULL可以存在多条未还记录一旦还了return_date 变成具体日期同一天归还同本书的记录只允许一条。这个方案在课程设计里够用但如果同一天多次借还同一本书这个唯一键会挡住第二次归还实际开发我会再加一个 borrow_status 字段或者把归还独立成流水表。答辩时能说出这个边界反而加分。2.3 引擎与外键InnoDB 是唯一合理选择建表时 ENGINE 选了 InnoDB这不是随意默认而是因为 MyISAM 不支持外键、不支持行级锁、不支持事务。图书管理系统只要涉及借书还书就必然要同时更新借阅表和库存表没有事务保证很容易出现“借阅记录插进去了库存没减”的脏数据。外键约束也是一样MyISAM 建外键不会报错但根本不生效删掉一个读者后借阅记录还留在表里数据完整性就崩了。InnoDB 的行级锁和事务是后面处理并发借还的基础。可能有人会提“性能差”但在课程设计这种几千行数据的场景里事务带来的开销完全可以忽略数据库优化的话题留到容量设计时再谈。选 InnoDB 还有一个实际好处Navicat 这类图形工具在可视化外键关系、生成 ER 图时只对 InnoDB 表支持得最完整对后续写文档和答辩展示都有帮助。不要为了标新立异去实验其他引擎这个场景没有理由不选 InnoDB。3. 增删改查打通借阅流程SQL 从单表到多表3.1 基础 CRUD先把单表操作练扎实图书管理系统本质上就是一组围绕三张表的增删改查。新增读者和新增图书的 SQL 没什么难度但有几个参数值得注意。INSERT INTO reader (reader_id, name, max_borrow) VALUES (2024001, 张三, 5); INSERT INTO book (title, author, stock, left_stock) VALUES (数据库系统概论, 王珊, 5, 5);第一条 SQL 只指定了三个字段created_at 交给 DEFAULT CURRENT_TIMESTAMP 自动填这是推荐写法不要手动塞时间。第二条插入图书时stock 和 left_stock 同时设为 5表示馆藏 5 本都可借。注意不能只写 stock 不写 left_stock否则 left_stock 会取默认值 1和总量对不上后面统计“还有几本可借”就错了。修改和删除同样有坑。UPDATE reader SET max_borrow 10 WHERE reader_id 2024001; DELETE FROM reader WHERE reader_id 2024001;UPDATE 语句务必带 WHERE而且要先用主键确认这个读者存在否则一次把全表的 max_borrow 都改了。DELETE 在有关联借阅记录时会失败这个我在第 5 章单独展开。单表 CRUD 看似简单但遵循一个习惯能少踩很多坑写 DELETE 或 UPDATE 之前先写一条等条件的 SELECT确认影响范围再改成 DELETE/UPDATE。这不是课程设计独有的习惯线上改数据我也是这么干。3.2 借书与还书先写数据库还是先写业务借书是整个系统最核心的流程它把“增删改查”从单表升级成了跨表事务。借书要完成三步校验读者和库存插入借阅记录扣减库存。这三步必须在一个事务里执行否则任何一步失败都会留下半边数据。我之前看到不少项目喜欢在业务层一个一个调 SQL先 INSERT borrow再 UPDATE book中间万一第二条 SQL 抛异常程序也不知道怎么撤销第一条。这就是典型的“先写业务后写数据库”顺序反了。正确的做法是把整段操作作为一个原子事务提交给数据库由数据库保证要么全成功要么全回滚。START TRANSACTION; SELECT left_stock FROM book WHERE book_id 1 FOR UPDATE; INSERT INTO borrow (reader_id, book_id, borrow_date, due_date) VALUES (2024001, 1, CURDATE(), DATE_ADD(CURDATE(), INTERVAL 30 DAY)); UPDATE book SET left_stock left_stock - 1 WHERE book_id 1; COMMIT;代码里的 FOR UPDATE 是行级排他锁锁住 book_id1 的这条库存记录防止两个事务同时读到“还有 1 本”然后各自扣减。CURDATE() 取当前日期DATE_ADD 算应还日期。借期 30 天是常见默认值也可以把天数做成参数。事务提交之后借阅表和库存表的数据一致这一步才算完成。还书流程是借书的逆向操作START TRANSACTION; UPDATE borrow SET return_date CURDATE() WHERE reader_id 2024001 AND book_id 1 AND return_date IS NULL; IF ROW_COUNT() 0 THEN ROLLBACK; END IF; UPDATE book SET left_stock left_stock 1 WHERE book_id 1; COMMIT;这里有个关键判断如果影响行数为 0说明没有未还的借阅记录不能再给库存加一必须回滚。很多同学写还书时不管三七二十一直接 UPDATE 两条还书还到一本没借过的书库存就越加越多了。参数上return_date 只能更新原本为 NULL 的记录用这个条件天然保证一笔借阅只还一次。3.3 查询统计借阅排行与逾期未还的 SQL 写法借阅查询是借阅表的主题新手最容易在这里迷路的多表 JOIN 反而最有成就感。借阅排行榜按读者分组统计借阅次数再关联读者表取姓名SELECT r.name, COUNT(*) AS borrow_count FROM borrow bor JOIN reader r ON r.reader_id bor.reader_id GROUP BY r.reader_id ORDER BY borrow_count DESC LIMIT 10;LIMIT 后面的参数表示取前 10 名课程设计里也可以改成一个可输入的变量。统计逾期未还书需要把当前日期和应还日期比较还要排除已经归还的记录SELECT r.name, b.title, bor.borrow_date, bor.due_date, DATEDIFF(CURDATE(), bor.due_date) AS overdue_days FROM borrow bor JOIN reader r ON r.reader_id bor.reader_id JOIN book b ON b.book_id bor.book_id WHERE bor.return_date IS NULL AND bor.due_date CURDATE() ORDER BY overdue_days DESC;DATEDIFF 在 MySQL 里返回两个日期相差的天数CURDATE() 是当天日期。注意如果用的是 SQL ServerDATEDIFF 的语法是 DATEDIFF(day, due_date, CURDATE())参数顺序相反这是换数据库时最容易踩的坑。两个查询都用到了 JOIN先通过外键关联 reader 和 book 把可读字段取出来再做过滤和排序顺序不能反否则先分组再关联容易把分组维度搞错。查询统计里还能加一个“当前借阅未还数量”SELECT r.name, COUNT(*) AS unreturned_count FROM borrow bor JOIN reader r ON r.reader_id bor.reader_id WHERE bor.return_date IS NULL GROUP BY r.reader_id HAVING unreturned_count r.max_borrow;HAVING 在这里替代 WHERE 过滤聚合结果门条件直接引用读者表里的 max_borrow判断谁已经借满了。这种写法在答辩时讲一句“用 HAVING 而不是 WHERE 是因为筛选的是聚合后的结果”就比单纯背概念有说服力得多。4. 数据库连接与并发JDBC 连接池和锁参数4.1 为什么课程设计也要用连接池如果整个系统只有一个人用、一分钟操作一次直接用 DriverManager.getConnection 也能跑。但课程设计答辩时老师往往会让多人同时测试或者连上数据库看实时日志这时候没有连接池的代码会在每个请求到来时新建物理连接高峰期数据库到达连接数上限直接拒绝服务。连接池的本质是复用若干条已经建立好的数据库连接请求来了从中取一条用完了归还而不是关闭。业界最常用的方案是 Druid 或者 HikariCP课程设计用 Druid 多一点因为它自带监控页面答辩时能展示连接池活跃数视觉效果很好。下面是一段最简配置用 Java 代码创建数据源不依赖 Spring 也能跑。DruidDataSource ds new DruidDataSource(); ds.setUrl(jdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8); ds.setUsername(root); ds.setPassword(123456); ds.setInitialSize(2); ds.setMaxActive(10); ds.setMaxWait(3000);这段代码里 setUrl 的characterEncodingutf8是必填项少了它中文写入数据库就会变成问号。initialSize 是启动时预先建立的连接数设为 2 够用maxActive 是连接池最大连接数课程设计 10 足够maxWait 是拿连接的超时毫秒数3000 表示等 3 秒拿不到就抛异常避免线程无限阻塞。如果是 HikariCP参数名变成 maximumPoolSize 和 connectionTimeout语义一样。4.2 连接池参数四个必调参数怎么定连接池不是配完就完参数之间互相影响乱调会出诡异问题。我整理了一张课程设计常用的参数表参数常见取值作用调参依据initialSize2~5启动时预先建立的连接数启动后第一次操作的等待时间太长就调大maxActive10~20允许的最大连接数数据库本身连接上限和你预期的并发用户数minIdle2最少保持的空闲连接长期空闲时回收连接以降低数据库压力maxWait3000~5000获取连接的最大等待毫秒数连接池满了程序不能无限等下去validationQuerySELECT 1借出连接前的有效性检测数据库重启后能自动淘汰失效连接最容易被忽略的是 validationQuery。数据库宕机重启后连接池里的旧连接其实已经断了如果不检测下一次 getConnection 拿到的是一个坏连接程序会报奇怪的通信异常。加一句SELECT 1作为探活 SQL逻辑上开销极小却能极大减少“连接失效”类玄学报错。另外maxActive 不要一味调大数据库并发连接数受max_connections限制调得比数据库上限还大连接都堆在排队反而拖垮数据库。4.3 避免数据库死锁借书场景的锁顺序连接池解决了连接复用但并发下真正的杀手是数据库死锁。借书事务要更新 book 表、插入 borrow 表假如两个人同时借两本书一个先锁 book 1 再锁 book 2另一个先锁 book 2 再锁 book 1两个事务互相等待对方释放锁就死锁了。死锁现象很典型日志里报Deadlock found when trying to get lock; try restarting transaction。解决死锁不是靠数据库自动重试而是让所有事务按同一顺序加锁。图书管理系统里借书操作先锁 book 表的一行再写 borrow 表还书操作也一样先锁 book 行再更新 borrow 表保证全局锁顺序一致。具体到 SQL在借书事务里用 SELECT ... FOR UPDATE 锁住 book 行后面所有操作都按这个顺序执行就不会形成循环等待。还有一种更彻底的兜底更新库存时使用条件更新减少锁持有时间。UPDATE book SET left_stock left_stock - 1 WHERE book_id 1 AND left_stock 0;这条 UPDATE 本身会锁住命中的行left_stock 0条件让它在没有库存时直接影响 0 行不用提前 SELECT 一把锁再判断。代码里收到影响行数为 0就知道借阅失败回滚事务。这样既减少了锁持有时长又防止库存负数。并发调优时把这几条 SQL 放到 JMeter 里压 50 个线程同时借同一本书观察 book 表的 left_stock 是否始终不小于 0就能验证方案是否正确。5. 避坑/常见问题排查图书管理系统从 .doc 到可运行5.1 现象Navicat 里跑得好Java 程序里中文全变问号原因几乎都出在连接字符集和数据库字符集不一致。Navicat 客户端会自动协商字符集所以你看不到问题Java 程序里如果 JDBC 连接串没写characterEncodingutf8驱动会拿客户端默认编码去解析中文一到数据库就成了乱码。 解决建库时用DEFAULT CHARSET utf8mb4连接串统一加useUnicodetruecharacterEncodingutf8。别只改一边连接串、数据库、表三处都要一致。如果项目用了连接池检查配置里是否被覆盖成了 latin1。修复后重启应用再插入一条中文书名用 SELECT 直接查库验证这一步能省掉后面所有关于乱码的折腾。5.2 现象删除图书报错 “Cannot delete or update a parent row”原因borrow 表里还有借阅记录外键引用这本书外键约束默认不允许删除被引用的父行。这是外键在起作用不是配置错误。 解决先处理借阅记录。如果这本书已经全量归还可以先把对应 borrow 里的 return_date 补齐再改为删除 borrow 记录最后删 book如果存在未还记录业务上不应该允许删。不要在设计表时图省事给外键加ON DELETE CASCADE否则删一本被借的书会把借阅历史全部连带删光答辩时讲不清数据追溯。课程设计阶段的正确做法是在程序里先查询SELECT COUNT(*) FROM borrow WHERE book_id ? AND return_date IS NULL有未还记录就拒绝删除并提示。5.3 现象还书日期计算不准逾期判断差一天原因很多人习惯在 Java 里用new Date()加减 30 天直接用毫秒数30 * 24 * 60 * 60 * 1000计算遇到夏令时、跨月、闰年都会算错而且数据库里的 DATE 和 Java 的 Date 之间存在时区偏移。逾期查询用了due_date CURDATE()因为 due_date 被加了一小时刚好跨过当天边界。 解决日期运算全部交给数据库插入借阅记录时用DATE_ADD(CURDATE(), INTERVAL 30 DAY)计算应还日期查询逾期用DATEDIFF(CURDATE(), due_date)Java 端只负责展示。如果必须在 Java 里处理用LocalDate.now().plusDays(30)不要用Date和毫秒加减。这个坑在答辩里被问到的概率很高因为期末时间点正好是跨月。5.4 现象两个人同时借最后一本书库存变成负数原因业务代码先SELECT left_stock判断大于 0再UPDATE扣减。两个事务同时读到 left_stock1都判断可借然后各自扣减库存变成 -1。这是典型的并发竞态跟事务隔离级别无关根因是“先查后改”没有锁保护。 解决把判断和扣减合并到一条 UPDATE 里用WHERE left_stock 0保证只有库存足够时才能更新成功。影响行数为 0 就说明抢不到库存事务回滚并提示“已被借完”。同时给借阅插入前的 reader 校验加FOR UPDATE锁防止超借。压测 50 个并发后left_stock 永远大于等于 0这条才算是真正过了。5.5 现象数据库同步后统计报表和业务库数字对不上原因做课程设计的人常把“.sql 文件导出再导入”当成同步结果只导了表数据和表结构忘了视图、触发器、存储过程或者用了只同步数据的数据库同步工具导致统计口径缺失。还有一种情况是同步过程中借阅表插入了新记录而统计视图是基于旧数据物化的两边自然对不上。 解决同步前使用 Navicat 的“转储 SQL 文件”功能勾选包含视图、触发器、事件目标是 MySQL 就用 mysqldump 全库导出不要只导表。同步完成后在目标库跑一遍第 3 章的逾期统计和借阅排行和源库比对数量是否一致。再做一次“先同步数据再同步结构”的反向验证。记住同步工具同步的是数据结构变化必须靠 SQL 脚本管理靠全库比对才能发现遗漏。6. 让课程设计超出预期视图、触发器与数据初始化脚本6.1 用视图封装“逾期未还”统计课程设计里最出彩的一步是把前面那段复杂的逾期查询封装成一个视图之后每次查询只需要SELECT * FROM overdue_view。视图在数据库里以虚拟表形式存在调用时实时查底层表数据永远是新的不需要考虑同步问题。CREATE VIEW overdue_view AS SELECT r.name AS reader_name, b.title AS book_title, bor.borrow_date, bor.due_date, DATEDIFF(CURDATE(), bor.due_date) AS overdue_days FROM borrow bor JOIN reader r ON r.reader_id bor.reader_id JOIN book b ON b.book_id bor.book_id WHERE bor.return_date IS NULL AND bor.due_date CURDATE();视图建好后报表查询从三段 JOIN 简化为一行指令程序代码里也不再散落业务 SQL。答辩时能说明“视图本质是保存的 SQL 查询逻辑不是拷贝的数据”对数据库知识点的评价会高很多。6.2 用触发器维护库存在借阅表上建触发器让库存的增减跟随借还记录自动完成这样程序层连手动 UPDATE 库存的代码都可以省掉。DELIMITER // CREATE TRIGGER trg_borrow_after_insert AFTER INSERT ON borrow FOR EACH ROW BEGIN UPDATE book SET left_stock left_stock - 1 WHERE book_id NEW.book_id; END// CREATE TRIGGER trg_borrow_after_update AFTER UPDATE ON borrow FOR EACH ROW BEGIN IF NEW.return_date IS NOT NULL AND OLD.return_date IS NULL THEN UPDATE book SET left_stock left_stock 1 WHERE book_id NEW.book_id; END IF; END// DELIMITER ;注意 DELIMITER 的用法MySQL 默认以分号作为语句结束符触发器体内包含分号必须先把结束符改成 //建完再改回来。第一个触发器在插入借阅记录后自动扣库存第二个触发器只在 return_date 从 NULL 变为非 NULL 时加库存避免同一行记录被重复触发。创建触发器后原有的手写库存更新业务可以移除但事务仍然要保留触发器参与的事务与外层事务共享同一个提交范围任意一步失败整体回滚。触发器不是越多越好。如果库存字段可以被人为修正或者批量导入历史借阅数据触发器会瞬间把库存扣成负数。建议只让触发器负责“借还动作”引起的库存变化初始化数据时先直接指定 left_stock 的最终值再插入借阅历史。6.3 初始化脚本造一批能用的测试数据课程设计最尴尬的场景是答辩时打开系统页面上只有一条测试记录。我习惯写一个初始化脚本一次性插入几十位读者、几十本书和一批有借有还的借阅记录让统计查询有数据可展示。INSERT INTO reader (reader_id, name) VALUES (2024001, 张三), (2024002, 李四), (2024003, 王五); INSERT INTO book (title, author, stock, left_stock) VALUES (数据库系统概论, 王珊, 5, 5), (计算机网络, 谢希仁, 3, 3), (操作系统原理, 汤小丹, 4, 4); INSERT INTO borrow (reader_id, book_id, borrow_date, due_date, return_date) VALUES (2024001, 1, 2025-03-01, 2025-03-31, 2025-03-28), (2024002, 2, 2025-03-05, 2025-04-04, NULL), (2024003, 3, 2025-04-01, 2025-05-01, NULL);造数据时有三个规律借阅日期要覆盖已还和未还两种状态应还日期要包含一个已经逾期的时间点让逾期视图能查出结果图书的 stock 与 left_stock 之差要和“当前借出数量”吻合。这三条对不上统计数字就破绽百出。初始化脚本最好放在独立的 init_data.sql 里不跟建表脚本混在一起方便以后清空数据重新演示。资质好的还能把初始化脚本和视图、触发器一起放进 .doc 附录显得整个过程是成体系的。我在做这个课程设计时养成了一个习惯所有用到的 SQL 都先写进独立的 .sql 文件再用 Java 调用而不是把 SQL 字符串直接埋在业务代码里。出问题时把 SQL 单独拿到 Navicat 里跑一遍问题在哪一层立刻分开。这个习惯帮我躲过了无数次“代码没动数据库重跑一下就好了”的玄学事故。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?