基于MySQL与IDEA的图书管理系统:数据库设计、JDBC事务与Swing实现

发布时间:2026/10/22 11:45:33
基于MySQL与IDEA的图书管理系统:数据库设计、JDBC事务与Swing实现 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, status2.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 层负责执行 SQLService 层负责组合多个 DAO 操作并控制事务UI 层只负责展示和接收输入。这种分层看起来有点“重”但对于后续维护和调试非常有用。当你发现某个功能出问题时能快速定位是 SQL 写错了、业务逻辑漏了判断还是界面传参传错了。3.2 数据库连接配置与连接池的引入数据库连接信息我放在一个db.properties文件里而不是硬编码在 Java 代码中。这样做的好处是换一台机器部署时只需要改配置文件不用重新编译。配置项包括url、username、password、driver。url里我加了useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8这几个参数。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 而不是 JavaFXSwing 和 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发现没有加characterEncodingutf8加上之后控制台显示正常了但界面还是乱码。第三步检查 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。后来我在建表语句里显式加上ENGINEInnoDB确保事务支持。这个坑的排查难度在于代码逻辑看起来完全正确但事务就是不生效。如果你也遇到类似情况先执行SHOW TABLE STATUS确认表的引擎类型再检查代码。问题现象可能原因排查方法解决方案中文显示问号编码不一致检查数据库、URL、IDE 编码统一设为 UTF-8无法获取连接连接泄漏SHOW PROCESSLIST查看 Sleep 连接检查 finally 块是否关闭连接事务不回滚存储引擎不支持SHOW TABLE STATUS查看引擎建表时指定 InnoDB7. 这套系统还能怎么扩展几个实用的方向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 项目里都是通用的。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询