简介一套数据库课程设计“图书管理系统”完整项目包面向计算机相关专业学生、毕业设计者及需要巩固数据库开发的自学者。资源同时呈现GUI与B/S两种常见架构既有Java Swing窗体程序源码也有JSP Web端代码配合SQL Server 2005数据库脚本清晰演示图书管理、读者管理、借阅归还等典型业务场景中的数据增删改查与关系设计。包内共168个文件其中jsp、java、class三类代码文件合计117个构成项目主体gif、jpg、png等图片用于界面元素sql脚本可快速建库建表doc与txt文档提供设计思路或配置说明压缩包仅1.38MB便于下载与离线使用。已有6927人浏览学习。通过这套资源可以对比桌面版与Web版在页面交互、数据访问方式上的差异理解数据库表结构设计及JDBC/Servlet等连接方法是课程设计或数据库原理实践的有价值参考。1. 图书管理系统一门数据库课程设计为什么值得认真做很多人把“数据库课程设计 图书管理系统”当成一个“做个界面、存点数据”的作业草草交了了事。实际上这个题目是数据库基本功的一次集中验收一张表怎么设计才算不别扭借书还书为什么必须动事务数据量到几万条时为什么查询开始变慢两个同学同时借同一本书时系统会不会出错——这些全都能在这一个系统里暴露出来。认真做完一遍相当于把课堂上的范式、索引、事务、并发控制从头到尾亲手验证了一次。这篇文章面向两类人一是正在选课设题目、想把这个题做扎实的学生二是想靠一个小项目补数据库实战经验的转行者。我会按自己的习惯从选型、建表、代码、界面到踩坑把一套能跑通、能答辩的完整方案讲清楚。2. 先定技术栈和表结构三个决定影响后面所有代码2.1 技术栈怎么选为什么最稳的组合是 Java MySQL JDBC课程设计的第一道坎是选型。见过太多人一上来就追新框架结果一半时间耗在配环境上。数据库课程设计的核心是数据库本身不是框架炫技所以“老师能看懂、环境好搭、资料多”比什么都重要。我一般推荐三个组合按稳妥程度排序组合适合人群风险点Java MySQL JDBC Swing学过 Java 的在校生界面老气但功能稳Java MySQL Spring Boot 网页前后端都碰过的学生配置多答辩加分Python SQLite/MySQL FlaskPython 为主的学生SQLite 和 MySQL 语法差异要小心这里我默认按“Java MySQL JDBC”往下讲这也是数据库课程设计最常见、资料最全的路线。MySQL 用 8.x 就好驱动用mysql-connector-javaIDE 用 IDEA 或 Eclipse 都行。数据库可视化工具用 Navicat 或者 DataGripNavicat 在课程设计圈子里用得多连接快、导入 SQL 脚本方便遇到“连接达梦数据库”“导入 Excel”这类需求它也能顺手处理。选 MySQL 而不是 SQLite 的理由SQLite 是单文件数据库做本地 demo 足够但它在并发写、用户权限、存储过程上都很弱课程设计要展示的“数据库增删改查”以外的内容——并发、事务、索引优化——它撑不起来。MySQL 至少让你有机会踩到真实的数据库坑而这些坑恰恰是答辩时的谈资。2.2 核心表设计图书表、读者表、借阅表三个实体两种关系表结构是一套系统的地基。图书管理系统数据库设计最常见的错误是只建一张大表所有字段塞进去结果数据冗余、改一处要动好几行。正常做法是拆成三张表图书表book存书的静态信息。book_id主键、title、author、publisher、isbn、category、location馆藏位置、total_stock总库存、available_stock可借数量。读者表readerreader_id主键、name、phone、reg_date、max_borrow最大可借数。借阅表borrow这是一张关联表记录“谁在哪天借了哪本、还了没有”。borrow_id主键、reader_id外键、book_id外键、borrow_date、due_date、return_date。为什么available_stock要单独冗余在图书表里而不是每次靠total_stock - 未还数量算出来因为“可借数量”这个字段在借书、还书两个操作里都会被高频查询每次实时计算要走一次子查询数据量大了以后非常吃亏。冗余一个字段用事务保证它和借阅记录同步更新查询时直接读出来就行。这是数据库设计里“用空间换时间”的典型取舍也是老师喜欢问的点。外键在课程设计里建议加上。有些生产环境为了性能会去掉外键依赖靠应用层保证一致性但那是大厂分布式架构的玩法。单机 MySQL 的课设老老实实建外键至少能让你在插入非法reader_id时直接被数据库拦下来少写一堆防御代码。2.3 把 E-R 图落成建表 SQL一份可以直接执行的脚本设计完表结构下一步是写建表 SQL。以下是一份可以直接放进 Navicat 或命令行执行的脚本-- 数据库课程设计图书管理系统建表脚本 -- 目标数据库MySQL 8.x字符集统一 utf8mb4 CREATE DATABASE IF NOT EXISTS library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE library_db; -- 图书表 CREATE TABLE book ( book_id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL COMMENT 书名, author VARCHAR(100) NOT NULL COMMENT 作者, publisher VARCHAR(100) DEFAULT NULL COMMENT 出版社, isbn VARCHAR(20) UNIQUE COMMENT ISBN号, category VARCHAR(50) DEFAULT 未分类 COMMENT 分类, location VARCHAR(50) DEFAULT NULL COMMENT 馆藏位置, total_stock INT NOT NULL DEFAULT 1 COMMENT 总库存, available_stock INT NOT NULL DEFAULT 1 COMMENT 可借数量, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_category (category) ) ENGINEInnoDB COMMENT图书表; -- 读者表 CREATE TABLE reader ( reader_id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL COMMENT 姓名, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, reg_date DATE DEFAULT (CURRENT_DATE) COMMENT 注册日期, max_borrow INT NOT NULL DEFAULT 5 COMMENT 最大可借数量 ) ENGINEInnoDB COMMENT读者表; -- 借阅表 CREATE TABLE borrow ( borrow_id INT AUTO_INCREMENT PRIMARY KEY, reader_id INT NOT NULL, book_id INT NOT NULL, borrow_date DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 借出时间, due_date DATETIME COMMENT 应还时间, return_date DATETIME DEFAULT NULL COMMENT 实际归还时间, 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_reader_borrow (reader_id, return_date) ) ENGINEInnoDB COMMENT借阅记录表;这份脚本里有几个地方是刻意设计的答辩时能讲出道理utf8mb4而不是utf8因为utf8在 MySQL 里最多存 3 字节遇到生僻字或 emoji 直接报错utf8mb4是完整实现。available_stock的初始值和total_stock一致保证插入图书时不用额外写 UPDATE。isbn加UNIQUE约束同一本书的不同副本不能重复录入。idx_category是给“按分类查书”准备的后面查询优化那节会讲到。外键名fk_borrow_reader显式命名将来要删约束时不用去翻系统表。执行的时候用 Navicat 连接本地 MySQL新建查询把整段脚本粘进去运行即可。如果之前建过同名数据库要先DROP DATABASE IF EXISTS library_db否则会报database already exists。2.4 连接池必须用为什么不能每次操作都新建数据库连接很多初学课设的代码长这样每次点按钮DriverManager.getConnection()用完close()。单机 demo 跑起来没感觉但答辩演示时连续点几十次“查询”界面卡顿数据库那边报Too many connections这就是经典的“数据库连接池没做”的翻车现场。建立 MySQL 连接要经过 TCP 握手、认证、分配线程资源一次连接的开销可能是普通 SQL 执行的几十倍。正确做法是用连接池让一批连接常驻复用。Java 课设里我一般用 HikariCP它是目前口碑最好的连接池配置少、性能好。以下是 HikariCP 在课程设计里的最小配置// 数据库连接池工具类基于 HikariCP // 引入依赖com.zaxxer:HikariCP import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; public class DbPool { private static final HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/library_db?useSSLfalseserverTimezoneAsia/Shanghai); config.setUsername(root); config.setPassword(your_password); config.setMaximumPoolSize(10); // 池中最大连接数 config.setMinimumIdle(5); // 最少保持的空闲连接 config.setConnectionTimeout(3000); // 获取连接超时时间(ms) config.setMaxLifetime(1800000); // 连接最大存活时间(ms) dataSource new HikariDataSource(config); } // 所有 DAO 通过这个方法获取连接用完归还不是关闭 public static java.sql.Connection getConnection() throws Exception { return dataSource.getConnection(); } }这段代码的关键点MaximumPoolSize在课设规模下 10 就够了开太大反而浪费 MySQL 的资源MaxLifetime要小于 MySQL 侧的wait_timeout否则连接被 MySQL 空闲断开池子里还留着失效连接下一次查询直接报错。这是一条血泪经验后面避坑节还会提到。3. 增删改查和事务边界借书还书为什么不能分开写3.1 DAO 层把数据库增删改查封装成五个基础方法表结构有了连接池有了接下来是数据访问层。课程设计的代码不需要过度设计但至少应该分层DAO 只管 SQL界面不直接碰Connection。以图书表为例一套最小的 DAO 长这样import java.sql.*; import java.util.ArrayList; import java.util.List; public class BookDao { // 查询全部图书按书名模糊匹配可选 public ListBook queryBooks(String keyword) throws Exception { ListBook list new ArrayList(); String sql SELECT * FROM book WHERE title LIKE ? ORDER BY book_id; try (Connection conn DbPool.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Book b new Book(); b.setBookId(rs.getInt(book_id)); b.setTitle(rs.getString(title)); b.setAuthor(rs.getString(author)); b.setAvailableStock(rs.getInt(available_stock)); list.add(b); } } } return list; } // 插入新图书检查 ISBN 重复由数据库唯一约束兜底 public int insertBook(Book book) throws Exception { String sql INSERT INTO book (title, author, publisher, isbn, category, location, total_stock, available_stock) VALUES (?, ?, ?, ?, ?, ?, ?, ?); try (Connection conn DbPool.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, book.getTitle()); ps.setString(2, book.getAuthor()); ps.setString(3, book.getPublisher()); ps.setString(4, book.getIsbn()); ps.setString(5, book.getCategory()); ps.setString(6, book.getLocation()); ps.setInt(7, book.getTotalStock()); ps.setInt(8, book.getTotalStock()); // 初始可借总库存 return ps.executeUpdate(); } } // 更新图书信息 public int updateBook(Book book) throws Exception { String sql UPDATE book SET title?, author?, publisher?, category?, location? WHERE book_id?; try (Connection conn DbPool.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, book.getTitle()); ps.setString(2, book.getAuthor()); ps.setString(3, book.getPublisher()); ps.setString(4, book.getCategory()); ps.setString(5, book.getLocation()); ps.setInt(6, book.getBookId()); return ps.executeUpdate(); } } // 删除图书有未归还借阅时禁止删除 public int deleteBook(int bookId) throws Exception { String sql DELETE FROM book WHERE book_id? AND NOT EXISTS (SELECT 1 FROM borrow WHERE book_id? AND return_date IS NULL); try (Connection conn DbPool.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, bookId); ps.setInt(2, bookId); return ps.executeUpdate(); // 返回0表示存在未还书删除被拒绝 } } }这里有几个细节值得展开第一全程用PreparedStatement而不是Statement。字符串拼接 SQL 是课程设计里最常见的坏味道不仅容易被注入而且每次执行都要重新解析 SQL。预编译的好处是 SQL 结构固定参数走?占位MySQL 可以复用执行计划。第二try-with-resources写法确保PreparedStatement和ResultSet自动关闭。注意关闭ps和rs时连接会归还给 HikariCP 连接池而不是真的断开这正是连接池的用法。第三删除图书时的NOT EXISTS子查询是业务规则一本还有没归还副本的书不允许删。这个逻辑放 SQL 里比放 Java 代码里更可靠避免“查到书了、删的时候刚好有人还书”的竞态。3.2 借书事务库存扣减和借阅记录必须同生共死借书这个动作涉及两步操作UPDATE book SET available_stock available_stock - 1和INSERT INTO borrow。初学者常犯的错是分了两个方法各自提交结果第一步成功、第二步失败库存少了但借阅记录没有——这就是经典的“数据库死锁”和“数据不一致”的温床。正确做法是手动控制事务边界。JDBC 里Connection默认是自动提交模式每句 SQL 一个事务。要合并多个操作必须setAutoCommit(false)全部成功后再commit()任何一步失败就rollback()// 借书扣库存 写借阅记录必须在同一个事务里 public void borrowBook(int readerId, int bookId, int borrowDays) throws Exception { String updateStockSql UPDATE book SET available_stock available_stock - 1 WHERE book_id ? AND available_stock 0; String insertBorrowSql INSERT INTO borrow (reader_id, book_id, due_date) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL ? DAY)); Connection conn null; try { conn DbPool.getConnection(); conn.setAutoCommit(false); // 开启手动事务 try (PreparedStatement ps1 conn.prepareStatement(updateStockSql)) { ps1.setInt(1, bookId); int rows ps1.executeUpdate(); if (rows 0) { throw new RuntimeException(库存不足借书失败); } } try (PreparedStatement ps2 conn.prepareStatement(insertBorrowSql)) { ps2.setInt(1, readerId); ps2.setInt(2, bookId); ps2.setInt(3, borrowDays); ps2.executeUpdate(); } conn.commit(); // 两步都成功一起提交 } catch (Exception e) { if (conn ! null) { conn.rollback(); // 任何一步失败回滚到借书之前 } throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); // 恢复默认归还连接 conn.close(); } } }这个方法的精髓在WHERE book_id ? AND available_stock 0。这行条件不仅做校验还利用数据库的行级锁保证并发安全两个同学同时借同一本书时第一个事务锁住这行、扣减库存第二个事务执行到同一句 UPDATE 时会被阻塞等第一个提交后它发现available_stock 0更新 0 行抛出“库存不足”。这就是为什么不能用“先 SELECT 查库存再 UPDATE 扣减”的两步走——那会引入查询和更新之间的时间窗口在这个窗口里并发请求会把库存打成负数。还书是借书的镜像操作UPDATE book SET available_stock available_stock 1加UPDATE borrow SET return_date NOW() WHERE borrow_id ?。同样包在一个事务里这里不重复贴代码逻辑完全对称。3.3 查询优化索引该建在哪慢查询怎么定位图书管理系统的数据量可能不大但课程设计如果只做到“能跑”答辩时老师问“你这系统将来存 10 万本书会不会慢”就答不上来。至少要能讲清楚两件事索引和慢查询。索引不是越多越好。每建一个索引写入时都要多维护一棵 B 树。图书查询的高频场景一般是按书名模糊查、按分类查、按 ISBN 精确查。按这个使用频率book表建idx_category就够了因为title LIKE %xxx%这种前缀模糊查询没法走普通 B 树索引建了也白建。如果确实要优化模糊搜索可以用LIKE xxx%前缀匹配这样能利用索引。验证索引有没有生效用EXPLAIN-- 查看查询执行计划确认走了哪个索引 EXPLAIN SELECT * FROM book WHERE category 计算机; -- 全表扫描对比 EXPLAIN SELECT * FROM book WHERE title LIKE %数据库%;第一句会看到key列有值idx_category第二句大概率是NULL说明在扫全表。这就是“为什么建了索引查询还是慢”的标准答案索引列上用函数运算或者前导通配符会让索引失效。慢查询的排查思路打开 MySQL 的慢查询日志或者用SHOW PROCESSLIST看正在执行的语句。如果发现某条 SQL 频繁出现把它拿出来EXPLAIN一遍优先看type列——ALL是全表扫描ref或range说明走了索引。把这条信息写进设计文档里答辩时是实打实的数据库优化证据。4. 界面怎么做Swing、JavaFX 还是网页4.1 三种界面方案的取舍别在界面上耗掉三分之二的时间数据库课程设计最耗时间的其实不是数据库是界面。很多同学把大量时间花在调按钮颜色和布局上数据库本身反而没做透。界面方案应该在动手前就定好原则是“够用就好”。界面方案开发量效果推荐度Java Swing小朴实老师熟悉最推荐JavaFX中现代一点但环境配置麻烦有基础再选JSP/Servlet中浏览器访问答辩效果好时间充裕可选Spring Boot Thymeleaf大最完整但超出课设范围不推荐新手Swing 最大的优势是 JDK 自带、不需要额外前端知识、单窗口程序容易讲清楚。别嫌它老课程设计考的是数据库不是 UI。如果非要用网页Servlet JSP 是折中方案前后端不分离一个BookServlet接收?actionquery之类的参数转发到 JSP 渲染结构和 JDBC 代码几乎一样只是把界面换成了 HTML。4.2 网页方案的最小结构一个 Servlet 处理四个动作选网页方案的话一个BookServlet就能覆盖核心的增删改查请求。路由设计如下请求路径参数动作/book?actionlistkeyword查询图书列表/book?actionadd表单字段新增图书/book?actioneditbookId 表单字段修改图书/book?actiondeletebookId删除图书Servlet 里用switch (action)分发到对应方法DAO 负责执行 SQLJSP 页面用 JSTL 标签渲染结果。这个结构的核心还是前面写的BookDao界面只是薄薄一层壳。需要提醒的是JSP 里不要在% %直接写 JDBC 代码那样页面和逻辑全耦合在一起改一个字段要动整个页面。哪怕课程设计也把 DAO 单独拎出来这是老师给分的重要依据。5. 避坑图书管理系统课程设计最常见的 6 个翻车点5.1 时区报错serverTimezone忘记配连接直接失败现象启动程序DbPool.getConnection()抛异常报错信息里有The server time zone value is unrecognized。原因MySQL 8.x 默认时区是 UTC而 JDBC 驱动要用本地时区做时间转换两边对不上就直接拒绝连接。解决JDBC URL 里加serverTimezoneAsia/Shanghai。另外注意useSSLfalse本地开发不需要 SSL加了反而出现证书告警。这两项是 JDBC 连接 MySQL 8.x 的标配。5.2 连接池连接失效跑着跑着突然报Communications link failure现象系统刚启动一切正常隔了半小时再点查询直接报连接失败重启程序又好。原因MySQL 的wait_timeout默认 8 小时但很多教材版的 MySQL 配置文件里会在空闲连接超过几分钟后主动断开。连接池里的连接被服务端掐断池子不知道还继续往外发。解决HikariCP 的maxLifetime设置比 MySQL 的wait_timeout短一点让池子主动淘汰失效连接。比如 MySQLwait_timeout是 3600 秒maxLifetime就设 1800000 毫秒30 分钟。这条经验同样适用于其他数据库连接池。5.3 外键约束导致的插入失败读者或图书还没存进去就报错现象新增借阅记录时报Cannot add or update a child row: a foreign key constraint fails。原因borrow表的reader_id或book_id在对应主表中不存在。常见场景是测试数据不完整或者删除图书时没检查借阅记录。解决借书前先校验两个 ID 的存在性或者直接把fk_borrow_reader这类约束的错误信息捕获后转换成“读者不存在”的中文提示。测试数据准备时要遵循插入顺序先读者、再图书、最后借阅。删除顺序反过来先删借阅再删书。5.4 自增主键越界测试数据删了又加主键冲到几十万现象明明只有 100 条数据book_id显示 100086。原因测试时反复新增、删除、回滚AUTO_INCREMENT不会因为删除而回退。解决课程设计演示前执行ALTER TABLE book AUTO_INCREMENT 1重新归位。注意如果表里还有数据重置后要和当前最大主键做协调否则会报Duplicate entry。这个小技巧能让答辩演示的数据整洁很多。5.5 中文字符乱码页面显示正常命令行查询全是问号现象程序里查出来的中文是???但直接连数据库查询又正常。原因连接层字符集和表字符集不一致。表用了utf8mb4但 JDBC URL 没指定字符集驱动用默认的latin1去做转码。解决JDBC URL 追加characterEncodingutf8。字符串utf8在 MySQL 驱动里会自动映射到utf8mb4。建表、连接、页面三个地方统一字符集乱码问题基本绝迹。6. 把系统做得像样一点并发准备、备份与答辩演示技巧到这里系统已经能跑了但距离“像样”还差几步。我会在交付前做三件事。第一件是补一个并发校验。借书前用SELECT max_borrow FROM reader查出读者最大可借数再SELECT COUNT(*) FROM borrow WHERE reader_id ? AND return_date IS NULL统计在借数量超过上限就拒绝。这两个操作记得放在同一个事务里并且对reader_id对应的行加锁可以用SELECT ... FOR UPDATE防止两个窗口同时借书时把借阅数量刷超。第二件是写备份脚本。课程设计不需要复杂的备份策略但至少要知道一条命令# 备份整个数据库到文件时间戳命名方便回溯 mysqldump -u root -p library_db library_db_backup_$(date %Y%m%d_%H%M%S).sql答辩前跑一次备份万一现场演示把数据弄乱了mysql -u root -p library_db backup.sql一条命令就能恢复这就是后悔药。第三件是演示前清理数据。把测试时留下的脏数据清掉重置自增主键插入几条典型的、方便讲故事的记录——比如一本《数据库系统概论》库存 3 本、借出 2 本现场演示“查书→借书→库存变化→还书”的完整闭环比满屏的乱数据有说服力得多。我的习惯是最后再检查一遍借书事务那段代码因为它是整个系统里唯一能在答辩现场玩出花的点老师问“两个人同时借最后一本书怎么办”你能当场指着available_stock 0条件讲清楚行锁和事务回滚这比背任何概念都管用。数据库课程设计就是这样选题人人相同拉开差距的永远是细节。希望这篇文章能帮你把每个该做的细节都做到位。本文还有配套的精品资源点击获取
