☰
基于MySQL与IDEA的图书管理系统:数据库设计、JDBC事务与Swing实现
2026/10/22 11:45:23 网站建设 项目流程

1. 从一张借阅卡说起:这个系统到底在解决什么问题

很多人第一次接触“图书管理系统”这个题目,脑子里浮现的画面大概是:一个表格,几行数据,点一下按钮就能借书还书。真动手做的时候才发现,事情远没有这么简单。我在实际搭建这套基于 MySQL 和 IDEA 的图书管理系统之前,也经历过“看起来很简单”的阶段,结果光是理清“一本书被借走之后,库存、借阅记录、读者状态这三者怎么同步”就花了大半天。

这套系统要解决的核心问题,其实可以拆成三个层面。第一层是数据的持久化存储,图书信息、读者信息、借阅记录不能只存在内存里,程序一关就没了,必须落到 MySQL 这样的关系型数据库里。第二层是业务逻辑的正确性,比如同一本书不能被两个人同时借走,借阅数量不能超过库存,还书之后库存要加回来。第三层是操作的便捷性,管理员要能快速增删改查,读者要能清楚看到自己借了什么、什么时候到期。

适合谁来参考这篇内容?如果你正在做课程设计、毕业设计,或者想找一个“麻雀虽小五脏俱全”的项目来练手数据库和 Java 桌面开发,这套系统非常合适。它不像电商系统那样庞大,但该有的东西一样不少:多表关联、事务控制、界面交互、异常处理。我见过太多人一上来就冲着“高并发”“分布式”去,结果连最基础的单表增删改查都写不利索。先把这套系统吃透,比什么都强。

在技术选型上,我用了MySQL 8.0作为数据库,IntelliJ IDEA作为开发工具,语言是Java,界面部分用的是Swing。为什么不用 Spring Boot 加前端框架?因为这套系统的定位是“桌面端单机应用”,引入 Web 框架反而会让结构变重,调试链路变长。Swing 虽然看起来“老”,但它和 JDBC 配合起来非常直接,适合把注意力集中在数据库设计和业务逻辑上。这一点在后面讲选型理由时我会再展开。

提示:如果你打算把这套系统作为课程设计提交,建议在文档里明确写出“为什么选择桌面端而不是 Web 端”,这往往是答辩时老师会追问的点。

2. 数据库表结构设计:三张核心表撑起整个系统

2.1 图书表、读者表、借阅记录表的分工

整套系统的数据模型围绕三张表展开:book(图书)、reader(读者)、borrow_record(借阅记录)。这三张表的关系是典型的“多对多通过中间表关联”——一本书可以被多个读者先后借阅,一个读者也可以借多本书,而borrow_record就是那个中间表。

先看book表的设计。我一开始只放了书名、作者、出版社,后来发现根本不够用。实际借阅场景里,库存数量和已借出数量必须分开记录,否则你没法判断“这本书还能不能借”。所以最终字段包括:book_id(主键,自增)、isbn(国际标准书号,唯一索引)、title、author、publisher、total_count(总库存)、available_count(可借数量)、location(馆藏位置)。这里有个细节:available_count不能单独维护,它应该等于total_count减去当前未归还的借阅记录数。但为了查询方便,我还是冗余了这个字段,代价是每次借书还书都要同步更新它。

reader表相对简单:reader_id(主键)、name、student_no(学号或工号,唯一)、phone、max_borrow(最大可借数量,默认 5)、status(状态,正常或冻结)。status这个字段是我后来加的,因为遇到过读者借了书长期不还的情况,管理员需要手动冻结他的借阅权限。

borrow_record表是整套系统的“流水账”:record_id(主键)、book_id(外键)、reader_id(外键)、borrow_date、due_date(应还日期)、return_date(实际归还日期,未归还时为 NULL)、status(借出中、已归还、逾期)。这张表的设计直接决定了“查询某人当前借了哪些书”“查询某本书当前被谁借走”这类操作能不能高效完成。

2.2 字段类型和约束的取舍细节

字段类型的选择上,我踩过一个小坑。isbn我一开始用了INT,结果发现 ISBN 有 13 位,而且可能以 0 开头,INT会把前导零吃掉。后来改成VARCHAR(20)才解决问题。borrow_date和due_date用DATE类型,因为借阅通常以“天”为单位,不需要精确到秒。但return_date我用了DATETIME,因为还书时间有时候需要精确记录,方便排查“到底是不是逾期”。

约束方面,外键约束我全部加上了,并且设置了ON DELETE RESTRICT。为什么不用CASCADE?因为如果删除一本书,连带把它的借阅记录也删掉,历史数据就丢了。图书管理系统的借阅记录是有留存价值的,不能随便级联删除。这一点在设计阶段就要想清楚,不然后期改起来很麻烦。

还有一个容易被忽略的点:索引。borrow_record表上,reader_id和book_id我都建了普通索引,因为“查询某读者的借阅历史”和“查询某本书的借阅历史”是最高频的操作。status字段也建了索引,因为“查询所有逾期记录”需要按状态过滤。这些索引在数据量小的时候感觉不出来,但当你插入几千条测试数据之后,查询速度的差异就非常明显了。

表名核心字段关键约束常用索引
bookbook_id, isbn, title, available_countisbn 唯一,available_count >= 0isbn 唯一索引
readerreader_id, student_no, statusstudent_no 唯一student_no 唯一索引
borrow_recordrecord_id, book_id, reader_id, status外键关联 book 和 readerreader_id, book_id, status

2.3 为什么没有用 ORM 框架

很多人会问:都什么年代了,为什么不用 MyBatis 或者 Hibernate?我的理由是:这套系统的 SQL 语句并不复杂,而且我需要精确控制每一条 SQL 的执行时机和事务边界。用原生 JDBC 虽然写起来啰嗦一点,但每一步都在自己掌控之中。比如借书操作,我需要在一个事务里完成“插入借阅记录”“更新图书可借数量”“检查读者借阅上限”三个动作,用 JDBC 的setAutoCommit(false)和commit()非常直观。如果套一层 ORM,反而要花时间去理解框架的事务传播机制。

当然,这不是说 ORM 不好。如果你的系统有几十张表、复杂的关联查询,那 ORM 能省很多事。但对于这套只有三张核心表的系统,原生 JDBC 加一个简单的 DAO 封装就足够了。我在 DAO 层写了一个BaseDao类,封装了获取连接、关闭资源、执行更新和查询的通用逻辑,子类只需要关注 SQL 和参数映射。这样既保持了轻量,又避免了大量重复代码。

3. 在 IDEA 里把 JDBC 链路跑通:从驱动加载到连接池

3.1 项目结构搭建与依赖引入

在 IDEA 里新建一个 Java 项目,我选的是 Maven 项目,因为管理依赖方便。pom.xml里只需要加一个mysql-connector-java依赖,版本选 8.0.33 或以上。如果你用的是 MySQL 8.0,驱动类名是com.mysql.cj.jdbc.Driver,不再是老版本的com.mysql.jdbc.Driver。这个细节很多人会搞错,导致启动时报ClassNotFoundException。

项目结构我分成了几个包:dao(数据访问层)、service(业务逻辑层)、ui(界面层)、entity(实体类)、util(工具类)。实体类就是和数据库表一一对应的 Java 类,字段名用驼峰命名,比如bookId对应book_id。DAO 层负责执行 SQL,Service 层负责组合多个 DAO 操作并控制事务,UI 层只负责展示和接收输入。这种分层看起来有点“重”,但对于后续维护和调试非常有用。当你发现某个功能出问题时,能快速定位是 SQL 写错了、业务逻辑漏了判断,还是界面传参传错了。

3.2 数据库连接配置与连接池的引入

数据库连接信息我放在一个db.properties文件里,而不是硬编码在 Java 代码中。这样做的好处是,换一台机器部署时只需要改配置文件,不用重新编译。配置项包括url、username、password、driver。url里我加了useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8这几个参数。serverTimezone不设置的话,MySQL 8.0 会报时区错误;characterEncoding不设置的话,中文书名会变成乱码。

一开始我用的是DriverManager.getConnection()每次直接获取连接,功能上没问题,但每次操作都要建立一次 TCP 连接,速度很慢。后来我引入了HikariCP连接池,配置了最小空闲连接数 5、最大连接数 20。连接池的好处是连接可以复用,借书还书这种高频操作响应速度明显提升。HikariCP 的配置也很简单,在db.properties里加几行参数,然后在工具类里初始化HikariDataSource即可。

注意:连接池用完之后一定要在程序退出时调用close()方法,否则可能导致连接泄漏,数据库端会看到大量睡眠连接。

3.3 一个容易被忽略的坑:事务边界与自动提交

JDBC 默认是自动提交模式,也就是说每执行一条 SQL 就立即生效。这在借书操作里会出大问题。假设我先插入借阅记录,成功了;然后更新图书可借数量,失败了。如果没有事务控制,借阅记录已经写进去了,但库存没减,数据就不一致了。

我的做法是在 Service 层的借书方法里,先调用connection.setAutoCommit(false),然后依次执行插入和更新,全部成功后调用commit(),任何一步抛异常就调用rollback()。这里有个细节:connection必须从连接池获取后一直传递到各个 DAO 方法,不能在每个 DAO 方法里各自获取连接,否则事务根本控制不住。我一开始就犯了这个错误,每个 DAO 方法内部自己拿连接,结果setAutoCommit(false)设了个寂寞。

正确的做法是:Service 层从连接池拿一个连接,把它传给 DAO 层的各个方法,DAO 方法用这个连接创建PreparedStatement,最后 Service 层统一提交或回滚,并在finally块里关闭连接(实际上是归还给连接池)。这个链路理顺之后,数据一致性问题就再也没出现过。

4. 借书还书背后的业务逻辑:不只是改两个数字

4.1 借书操作的完整校验链路

借书这个动作,表面上看就是“读者拿走一本书”,但在代码里需要经过至少五道校验。第一道,检查读者状态是否为“正常”,如果被冻结了直接拒绝。第二道,检查读者当前已借数量是否达到max_borrow上限。第三道,检查这本书的available_count是否大于 0。第四道,检查该读者是否已经借了同一本书且未归还(防止重复借阅)。第五道,检查通过后,才执行插入借阅记录和更新库存的操作。

这五道校验的顺序也有讲究。我一开始把“检查库存”放在最前面,后来发现如果读者已经被冻结了,根本没必要去查库存,白白多一次数据库查询。所以调整成了“先查读者状态和借阅数量,再查图书库存”。这种微小的顺序调整,在单次操作里感觉不出来,但当系统每天处理几百次借阅时,累积的效率提升就很可观了。

还有一个细节:due_date的计算。我默认借阅期限是 30 天,用LocalDate.now().plusDays(30)计算。但这里要考虑一个问题:如果读者在 1 月 31 日借书,30 天后是几号?用plusDays会自动处理月份天数差异,不会出现“2 月 31 日”这种非法日期。如果自己手动算,很容易出错。

4.2 还书操作与逾期判断

还书操作相对简单,但逾期判断需要仔细处理。当读者归还一本书时,系统先根据record_id找到对应的借阅记录,把return_date设为当前时间,status改为“已归还”,然后把图书的available_count加 1。逾期判断的逻辑是:如果return_date晚于due_date,则标记为“逾期归还”,并在界面上给出提示。

这里有个业务上的取舍:逾期要不要罚款?我在这个版本里没有做罚款功能,因为涉及到金额计算和支付流程,会让系统复杂度上升一个量级。但我预留了一个fine字段在borrow_record表里,默认值为 0,后续如果要扩展罚款功能,直接在这个字段上做文章就行。这种“预留扩展点”的做法,在实际开发中很常见,既不影响当前功能,又为未来留了余地。

另外,还书时我加了一个“确认对话框”,显示书名、读者名和应还日期,让管理员确认无误后再执行。这个设计是为了防止误操作——如果管理员点错了记录,直接把 A 读者的书还到了 B 读者名下,数据就乱了。多一步确认,少很多麻烦。

4.3 查询功能的实现:模糊搜索与分页

查询功能是图书管理系统里使用频率最高的。我实现了三种查询:按书名模糊搜索、按作者模糊搜索、按 ISBN 精确搜索。模糊搜索用LIKE '%关键词%'实现,但这里有个性能隐患:如果数据量很大,LIKE '%...%'无法使用索引,会全表扫描。对于课程设计级别的数据量(几千条),这不是问题,但如果要上生产环境,就需要考虑全文索引或者搜索引擎了。

分页功能我用的是LIMIT offset, size的方式。offset的计算公式是(pageNum - 1) * pageSize。这个公式看起来简单,但我在实际写的时候犯过一个错:把pageNum从 0 开始计数,结果第一页显示的是第二页的数据。后来统一改成从 1 开始计数,问题解决。分页查询还需要一个“总数查询”,用来计算总页数。我写了一个SELECT COUNT(*)的 SQL,和分页查询共用WHERE条件,保证总数和实际数据一致。

查询类型SQL 示例索引使用情况适用场景
书名模糊WHERE title LIKE '%Java%'无法使用索引数据量小,快速查找
ISBN 精确WHERE isbn = '9787111...'使用唯一索引已知书号,精确定位
借阅历史WHERE reader_id = ? ORDER BY borrow_date DESC使用普通索引查看某读者借阅记录

5. 界面层与业务层的解耦:Swing 也能写得清爽

5.1 为什么选择 Swing 而不是 JavaFX

Swing 和 JavaFX 都是 Java 桌面开发的选择。我选 Swing 的原因很实际:IDEA 对 Swing 的可视化设计支持更成熟,拖拽控件就能生成界面代码,省去了大量手写布局的时间。JavaFX 虽然更现代,但需要额外引入依赖,而且 FXML 的写法对于初学者来说学习曲线更陡。这套系统的界面并不复杂,主要是表格、按钮、输入框,Swing 完全够用。

界面结构上,我用了JFrame作为主窗口,里面放JTabbedPane分成“图书管理”“读者管理”“借阅管理”三个标签页。每个标签页里用JTable展示数据,配合JScrollPane实现滚动。表格的数据模型我自定义了一个BookTableModel,继承AbstractTableModel,这样表格的数据源和数据库查询结果能直接对接,刷新数据只需要重新查询并调用fireTableDataChanged()。

5.2 事件处理与线程安全

Swing 的事件处理有一个著名的坑:所有界面更新必须在事件分发线程(EDT)中进行。如果你在后台线程里直接修改JTable的数据模型,可能会遇到界面卡死或者数据错乱。我的做法是:数据库查询放在SwingWorker的doInBackground()方法里执行,查询结果通过done()方法回到 EDT 中更新界面。这样既保证了界面不卡顿,又避免了线程安全问题。

举个例子,当用户点击“查询”按钮时,SwingWorker在后台执行 SQL,查询期间界面上的按钮可以保持禁用状态,查询完成后按钮恢复,表格刷新。这个模式我封装成了一个通用的QueryTask类,所有查询操作都复用它,代码量减少了很多。

5.3 界面与业务层的接口设计

界面层不直接调用 DAO,而是通过 Service 层暴露的方法来操作数据。比如借书按钮的点击事件里,只调用borrowService.borrowBook(readerId, bookId),至于里面怎么校验、怎么开事务、怎么更新库存,界面层完全不关心。这样做的好处是,如果以后要把界面从 Swing 换成 Web 页面,Service 层和 DAO 层几乎不用改,只需要重写界面层即可。

Service 层的方法返回值我统一用了一个Result类,包含success(布尔值)、message(提示信息)、data(返回数据)。界面层根据success判断操作是否成功,根据message显示提示框。这种统一的返回格式,让界面层的代码非常简洁,不需要到处写 try-catch。

6. 调试过程中踩过的坑与排查思路

6.1 中文乱码问题的完整排查链路

中文乱码是我遇到的第一个问题。现象是:在 IDEA 控制台插入的中文书名,在 MySQL 命令行里查询显示正常,但在 Swing 界面里显示成问号。排查过程分三步。第一步,检查数据库和表的字符集,确认是utf8mb4。第二步,检查 JDBC 连接 URL,发现没有加characterEncoding=utf8,加上之后控制台显示正常了,但界面还是乱码。第三步,检查 IDEA 的文件编码设置,发现File Encodings里默认是 GBK,改成 UTF-8 之后问题彻底解决。

这个排查链路告诉我一个道理:乱码问题可能出现在多个环节,数据库、连接、IDE、操作系统,任何一个环节的编码不一致都会导致乱码。排查时要逐个环节确认,不要只盯着一个地方改。

6.2 连接池耗尽与连接泄漏的定位

有一次测试时,系统运行了十几分钟之后突然报“无法获取连接”。我一开始以为是连接池最大连接数设小了,把 20 改成了 50,结果问题依旧。后来用SHOW PROCESSLIST查看 MySQL 端的连接,发现大量连接处于Sleep状态,说明连接没有被归还。回头检查代码,发现有一个 DAO 方法在finally块里忘记调用connection.close(),导致每次查询都泄漏一个连接。修复之后,连接数稳定在 5 到 10 之间,再也没有出现过耗尽的情况。

这个坑的教训是:连接池不是万能的,用了连接池更要小心连接泄漏。因为连接池会掩盖泄漏问题,直到池子满了才暴露出来。我的建议是,在开发阶段把最大连接数设小一点(比如 5),这样泄漏问题会更快暴露,方便及早修复。

6.3 事务回滚失效的隐蔽原因

前面提到过,我一开始在每个 DAO 方法里各自获取连接,导致事务控制失效。但还有一个更隐蔽的原因:MySQL 的存储引擎。如果表用的是 MyISAM 引擎,它根本不支持事务,rollback()调用也不会报错,但数据已经写进去了。我检查了一下,发现建表时没有指定引擎,MySQL 5.7 默认是 InnoDB,但有些环境可能默认是 MyISAM。后来我在建表语句里显式加上ENGINE=InnoDB,确保事务支持。

这个坑的排查难度在于,代码逻辑看起来完全正确,但事务就是不生效。如果你也遇到类似情况,先执行SHOW TABLE STATUS确认表的引擎类型,再检查代码。

问题现象可能原因排查方法解决方案
中文显示问号编码不一致检查数据库、URL、IDE 编码统一设为 UTF-8
无法获取连接连接泄漏SHOW PROCESSLIST查看 Sleep 连接检查 finally 块是否关闭连接
事务不回滚存储引擎不支持SHOW TABLE STATUS查看引擎建表时指定 InnoDB

7. 这套系统还能怎么扩展:几个实用的方向

7.1 增加预约功能与到期提醒

目前的系统只支持“借”和“还”,但如果一本书被借走了,其他读者只能等。一个自然的扩展是增加“预约”功能:读者可以预约一本当前不可借的书,等书归还后系统自动通知预约者。实现思路是新增一张reservation表,记录预约的读者、图书和预约时间。当还书操作发生时,检查是否有预约记录,如果有,把书的状态改为“已预约”,并给预约者发送一条站内消息。

到期提醒也是一个实用功能。可以在系统启动时查询所有due_date在三天内且未归还的记录,在界面上弹出一个提醒列表。这个功能不需要定时任务,每次管理员登录时检查一次即可,实现简单但效果很好。

7.2 从单机版到网络版的改造思路

如果要把这套系统从单机版改成网络版,让多个管理员同时使用,核心改动点在于:把 Service 层和 DAO 层从客户端移到服务端,客户端只负责界面展示和网络通信。数据库连接池放在服务端,客户端通过 HTTP 或 Socket 调用服务端接口。这个改造的工作量不小,但架构上的变化是清晰的:原来的“界面 → Service → DAO → 数据库”变成了“客户端界面 → 网络 → 服务端 Service → DAO → 数据库”。

改造过程中最大的挑战是并发控制。多个管理员同时借同一本书时,需要加锁或者用乐观锁来防止超借。可以在book表加一个version字段,每次更新时检查版本号是否变化。这个思路和数据库的 MVCC 机制类似,实现起来不复杂,但能有效解决并发问题。

7.3 数据导出与报表生成

管理员经常需要把借阅记录导出成 Excel 或者打印报表。Java 里可以用 Apache POI 库来生成 Excel 文件,把borrow_record表的查询结果写入工作簿。报表方面,可以用 JasperReports 生成 PDF 格式的借阅统计报表,按月份、按读者、按图书分类统计。这些功能不需要改动核心业务逻辑,只是在现有数据之上做展示层的扩展。

我在实际使用中发现,导出功能最好加上“导出范围选择”,比如“导出本月记录”“导出某读者的全部记录”,而不是一股脑导出所有数据。这样既减少了导出时间,也让导出的文件更有针对性。

8. 一些个人体会:从这套系统里学到了什么

这套系统我前前后后重构了三次。第一次是功能堆砌,所有代码写在一个类里,改一个功能要翻几百行。第二次是分层重构,把 DAO、Service、UI 拆开,代码清晰了很多,但事务控制还是有问题。第三次是引入连接池和统一返回格式,才算真正稳定下来。这个过程让我深刻体会到:软件设计不是一开始就完美的,而是在不断发现问题、解决问题的过程中逐步演进的。

如果让我给正在做类似项目的朋友一条建议,那就是:先把数据库表结构设计好,再动手写代码。我见过太多人急着写界面,结果写到一半发现表结构不合理,回头改表、改代码,浪费的时间远比一开始多花半小时设计表结构要多。表结构是地基,地基不稳,楼越高越危险。

另外,不要害怕用“老”技术。Swing 也好,原生 JDBC 也好,它们可能不是最时髦的,但足够稳定、足够直接。把精力放在理解业务逻辑和数据库原理上,比追逐框架版本号有价值得多。这套系统里学到的多表关联、事务控制、连接池管理,放到任何 Java 项目里都是通用的。

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

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

立即咨询