ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java + Oracle 图书馆管理系统:从 JDBC 到事务、分页与排错全攻略

Java + Oracle 图书馆管理系统:从 JDBC 到事务、分页与排错全攻略 简介一份面向Java初学者的图书馆管理系统课程设计资源基于JavaOracle实现适合高校学生完成数据库课程设计或毕业设计入门。资源围绕图书管理、读者管理、借阅管理等核心业务覆盖JDBC数据库连接、控制台界面交互、异常处理、SQL建表与增删改查、MVC分层设计、文件日志记录等关键知识点并附有数据库课程设计说明文档便于对照理解开发全流程。压缩包共98个文件包含45个Java源文件、46个编译后的class文件、2个Oracle驱动jar包、1个SQL建库脚本以及project、classpath、properties等工程配置文件和说明文档整体仅5.54MB结构清晰、模块划分明确可直接导入开发环境查看运行。目前已有1277人学习下载对希望快速上手JavaOracle桌面应用开发、梳理MVC分层思路或完成课程设计的初学者是一份很有参考价值的实战样例。 最近我帮一个准备毕业设计的同学梳理过一套“图书馆管理系统”的完整落地思路用的就是 Java Oracle 这套组合。说实话这题目从我做课设那会儿就有二十年了依然是经典。但经典不代表简单恰恰因为它业务闭环完整——读者管理、图书信息、借阅归还、逾期罚金、统计报表全都有用来练架构、练 SQL、练排错都比市面上花里胡哨的电商项目扎实得多。尤其当你把数据库从 MySQL 换成 Oracle 之后事情就变得更有意思了。连接串不一样、分页写法不一样、事务行为不一样、大小写敏感度不一样甚至 JDBC 驱动的版本都能坑你半天。这篇文章我就按自己做这类系统的实际经验把从设计到编码再到排查的完整过程拆开讲清楚希望能让正在做课设、毕设或者准备面试的你少走几个月的弯路。1. 项目整体设计为什么这套选型最耐打很多初学 Java 的人一听到 Oracle 就有点发怵觉得那是金融、电信行业才玩得起的东西个人电脑上装个 Oracle 都很费劲更别说拿来做项目了。但我的观点恰恰相反——如果你已经会用 MySQL 做增删改查那用 Oracle 完成一版图书馆管理系统能把你对数据库的认知拔高一个档次。1.1 需求拆解管理系统到底在管什么我们先别急着写代码先把系统的功能边界画清楚。图书馆管理系统本质上是给两类人用的读者端查书、借书、还书、续借、查看自己的借阅记录。管理员端图书入库/下架、读者信息维护、处理逾期罚款、查看统计报表。如果再简化一点核心其实就是五个字借、还、查、管、统计。借书要检查读者状态和图书库存还书要算有没有逾期查询要支持书名和作者模糊搜索统计要能输出借阅排行榜和热门分类。这就是标准的最小可用系统你把这个做透了再往上面加预约、座位管理、消息通知都是后面的事。我当时做第一版时犯过一个错误就是功能规划得太大一上来就想着要做多角色权限、操作日志、批量导入导出结果画 ER 图画了两周代码一行没写。后来砍掉所有非核心需求只保留上面三张表能支撑的逻辑一周就跑通了。1.2 为什么推荐 Java Oracle 而不是 Java MySQL很多人问我毕设选那么重的 Oracle 图什么我说三层意思第一Oracle 的规范性强。它没有 MySQL 那种“随便写也能跑”的放纵感表名和字段名大小写敏感、字符串函数严格、事务隔离行为明确你在 Oracle 上写出来的 SQL 往往更规范这套习惯带到工作里非常值钱。第二面试能聊的东西多。你写上“熟悉 Oracle 分页、存储过程、序列、物化视图”面试官大概率会多问几句。而如果你写“熟悉 MySQL 增删改查”基本上就是被略过的份。第三学习成本换取性价比。Oracle 的安装确实比 MySQL 笨重但熬过安装那关之后你会逼着自己去理解实例、表空间、用户、权限、监听器这些概念。这些知识不是浪费它是数据库工程师的入门砖。所以我个人建议如果学校有条件、电脑配置允许尽量选 Oracle。没有条件的话装一个 Oracle XE快捷版也完全够跑这个小系统它对硬件的要求友善得多。1.3 项目架构别一上来就非要全家桶现在网上铺天盖地的 Spring Boot MyBatis 教程搞得人以为 Java 写代码必须这么来。可你去看实际的课设评分和面试考核老师更看重的是你对每一条 SQL、每一个业务逻辑的理解而不是你会不会用注解。如果我的目标是快速跑通并搞懂原理会推荐这套极简架构Web 层JSP Servlet 或 Spring MVC都能选。业务层Service 类一个方法对应一个业务动作。数据层JDBC直接手写连接和 SQL不引入 ORM。数据库Oracle 11g 或 19c。你可能觉得 JDBC 太原始了。但我要说的恰恰是正因为原始你才会认真对待连接什么时候关闭、事务边界设在哪、结果集怎么遍历。这些底子打扎实了以后用 MyBatis 只是换一种封装方式而已。真的人云亦云上了全家桶SQL 都是框架自动生成的出了问题反而无从排查。2. 数据建模与 Oracle 核心实践这个系统要跑得顺畅表设计是关键中的关键。Oracle 不像 MySQL 那样宽容表结构设计得好不好直接决定后面 SQL 写起来顺不顺手。2.1 三张核心表字段类型千万别乱选这类系统通常只需要三张业务表加几张码表读者表读者编号、读者姓名、电话、证件类型、注册日期、状态图书表图书编号、ISBN、书名、作者、出版社、分类、总库存、可借库存、馆藏位置借阅表借阅编号、读者编号、图书编号、借书日期、应还日期、实际归还日期、状态、罚款金额。听上去和 MySQL 差不多但字段类型有点讲究字段类型建议原因主键NUMBER(10)Oracle 没有真正的 INTNUMBER 才是通用整数类型书名/作者VARCHAR2(100)必须指定长度不能省略ISBNVARCHAR2(20)实际 ISBN 可能是带横线的字符串别用数字日期DATE默认带时分秒计算逾期天数很方便金额NUMBER(6,2)保留两位小数避免浮点误差库存数量NUMBER(6)保证非负加 CHECK 约束我当时吃过一个亏图书编号想图省事用字符串还在前端做成了自动编号“BOOK001”结果统计分类的时候还得到处 SUBSTR麻烦得要命。后来改成 sequence 数字主键一切瞬间清爽。记住业务上的编号可以用唯一约束去控制但数据库主键用单调数字永远是最稳的选择。2.2 主键生成的两种方式序列和触发器Oracle 不像 MySQL 有 AUTO_INCREMENT它的自增要靠序列SEQUENCE。最基本的用法是CREATE SEQUENCE seq_reader_id START WITH 1 INCREMENT BY 1 NOCACHE;插入数据时这样取号INSERT INTO readers(reader_id, reader_no, reader_name, phone, status) VALUES (seq_reader_id.NEXTVAL, R || seq_reader_id.NEXTVAL, 张三, 13800000000, 1);这里有个初学者常踩的坑同一个 SQL 里 NEXTVAL 每次调用都会递增如果你在一条 INSERT 里用了两次 NEXTVAL两个字段拿到的值就不一致。正确做法是先用变量取一次再放进 INSERT 里。另外如果你想实现真正的“自动”编号还可以配合触发器CREATE OR REPLACE TRIGGER trg_reader_auto_id BEFORE INSERT ON readers FOR EACH ROW BEGIN IF :NEW.reader_id IS NULL THEN SELECT seq_reader_id.NEXTVAL INTO :NEW.reader_id FROM dual; END IF; END;日常练习建议先掌握手动取号的写法触发器理解原理即可面试官更爱问 NEXTVAL 和 CURRVAL 的区别。2.3 Oracle 分页经典 ROWNUM 写法分页查询几乎是所有管理系统必备功能。MySQL 的 LIMIT 写多了一到 Oracle 就懵因为 Oracle 没有 LIMIT。老版本 Oracle 的标准分页是三层嵌套SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT book_id, book_name, author, publisher FROM books WHERE book_name LIKE % || :kw || % ORDER BY book_id DESC ) t WHERE ROWNUM :page * :size ) WHERE rn (:page - 1) * :size;为什么非要三层因为 ROWNUM 是在数据筛选过程中分配的如果你先 WHERE ROWNUM 10等于是让 Oracle 在还不知道结果集长度的时候就要求跳过前 10 行它做不到。必须先取到小于等于页码乘每页条数的结果再在外面一层用别名过滤掉前面的行。Oracle 12c 之后推出了行限制子句SELECT * FROM books ORDER BY book_id DESC OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;这个写法接近 MySQL 的 LIMIT简洁很多。但如果你要兼容 11g还是老老实实用 ROWNUM。面试被问到分页时把这层原理讲清楚比背一万个“八股”都管用。2.4 日期处理TRUNC 和 SYSDATE 的组合借阅业务大量依赖日期计算。比如判断一本书是否逾期标准公式就是“应还日期”和“系统当天日期”之间的差值SELECT borrow_id, reader_name, book_name, TRUNC(SYSDATE) - TRUNC(due_date) AS overdue_days FROM borrows b JOIN readers r ON b.reader_id r.reader_id JOIN books bk ON b.book_id bk.book_id WHERE return_date IS NULL AND TRUNC(SYSDATE) TRUNC(due_date);TRUNC(SYSDATE) 的作用是把时间截断到当天零点因为 DATE 类型默认带着时分秒如果你直接用 SYSDATE - due_date得到的是带小数的天数例如 3.54 天这显示给读者看就很奇怪。先用 TRUNC 把两边都截断到日期再做差值才是“过了几天”的准确答案。这里还要注意一点Oracle 里两个 DATE 相减的结果就是天数但如果两列是 TIMESTAMP相减的结果是 INTERVAL 类型不能直接用于乘法计算此时要用 EXTRACT(DAY FROM ...) 或者 CAST 转成 DATE 再算。3. 业务逻辑实现借书还书背后的细节表结构设计好SQL 能查出来接下来就是把业务串起来的关键环节。很多人的项目挂在“借书时库存没扣成功”或者“并发下多借了几本书”这种问题上本质上都是业务逻辑的事务边界没划清楚。3.1 连接管理一定用连接池别每次 new Connection初学者最容易犯的错就是方法里写一句DriverManager.getConnection(url, user, password)用完再关闭。在课设演示环境下这看起来没问题但一旦把并发量提高到几十个用户你就发现系统动不动卡死日志里全是无法创建新连接的报错。原因很简单建立数据库连接是重操作要完成网络握手、认证、会话初始化整个过程可能消耗几十毫秒甚至更久。如果每次请求都重复这个过程系统的大部分时间都耗在建立连接上而不是执行 SQL。连接池的作用就是提前维护一批连接请求来了直接复用。我推荐 HikariCP配置非常简单dataSource: driverClassName: oracle.jdbc.driver.OracleDriver jdbcUrl: jdbc:oracle:thin:localhost:1521:XE username: library password: library123 maximumPoolSize: 20 minimumIdle: 5 connectionTimeout: 30000maximumPoolSize别贪多20 足够支撑几百人的小系统。配太多反而浪费数据库资源因为 Oracle 默认的 process 数就那么点连接超过进程数上限照样报 ORA-12520。3.2 JDBC 连接串驱动别乱配连接 Oracle 的 JDBC URL 有两种写法区别在于你连接的是 SID 还是服务名// SID 方式常见于 11g String url jdbc:oracle:thin:localhost:1521:orcl; // 服务名方式常见于 12c/19c 的 PDB String url2 jdbc:oracle:thin:localhost:1521/orclpdb;注意冒号和斜杠的区别。SID 用冒号服务名用斜杠。这个细节我在帮别人排错时遇到过好几次他用的 19c复制网上的 SID 写法连不上改成服务名写法就通了。驱动版本也要对应Oracle 11g 用 ojdbc612c 用 ojdbc819c 用 ojdbc11。网上很多“连不上数据库”的问题十有八九就是你拿 ojdbc6 去连 19c或者反过来。驱动版本和数据库版本最好保持一致这不算玄学是官方兼容性要求。3.3 借书业务一个方法里的完整事务借书这个动作拆开看至少包含三件事查询读者状态和借书限额。查询图书的可借库存。扣减可借库存并且插入一条借阅记录。这三件事必须在一个事务里要么全成功要么全失败。Java 里的最小实现public void borrowBook(int readerId, int bookId) { String checkReaderSql SELECT status, borrow_count FROM readers WHERE reader_id ?; String checkBookSql SELECT available_count FROM books WHERE book_id ? FOR UPDATE; String updateBookSql UPDATE books SET available_count available_count - 1 WHERE book_id ?; String insertBorrowSql INSERT INTO borrows(borrow_id, reader_id, book_id, borrow_date, due_date) VALUES (seq_borrow_id.NEXTVAL, ?, ?, SYSDATE, SYSDATE 30); try (Connection conn dataSource.getConnection()) { conn.setAutoCommit(false); // 1. 查读者状态异常则抛出业务异常 // 2. 查图书用 FOR UPDATE 锁定这一行防止并发下重复扣减 // 3. 执行 UPDATE 和 INSERT conn.commit(); } catch (Exception e) { // 记得 rollback throw e; } }这里面最容易被忽视的是SELECT ... FOR UPDATE。不加锁的话两个用户同时借同一本库存只剩 1 的书两个事务都查到了 available_count1然后各自减 1最终可用库存变成 -1。加锁后第二个事务的查询会阻塞等第一个事务提交后才能看到新值从而正确识别库存不足。这行代码就是教科书里“并发控制”的最直观案例。3.4 还书与逾期罚金存储过程写在数据库还是应用层还书业务相对简单更新借阅记录的回车日期和状态把可用库存加回来。难点在逾期罚金计算。我推荐把罚金计算这类与数据库日期强相关的逻辑用一条 SQL 完成UPDATE borrows b SET return_date SYSDATE, status RETURNED, fine_amount CASE WHEN TRUNC(SYSDATE) TRUNC(due_date) THEN (TRUNC(SYSDATE) - TRUNC(due_date)) * 0.5 ELSE 0 END WHERE borrow_id ?;如果你非要用存储过程我也给出一个最小示例CREATE OR REPLACE PROCEDURE sp_return_book(p_borrow_id NUMBER) IS v_fine NUMBER; BEGIN SELECT CASE WHEN SYSDATE due_date THEN (TRUNC(SYSDATE) - TRUNC(due_date)) * 0.5 ELSE 0 END INTO v_fine FROM borrows WHERE borrow_id p_borrow_id; UPDATE borrows SET return_date SYSDATE, status RETURNED, fine_amount v_fine WHERE borrow_id p_borrow_id; COMMIT; END;我的建议是课设阶段用第一种业务逻辑放应用层更直观等到能应付“连接池连接和数据库会话”这层关系后再学存储过程也不晚。毕竟存储过程虽快但维护成本高团队协作时大家对它的理解容易有偏差。4. 常见问题与排查技巧实录这部分是重点。做这类系统时真正浪费时间的往往不是写代码而是那些看着像代码问题、其实是环境或配置问题的报错。我把这几年帮人排错遇到的典型问题列成清单每一条都值得收藏。4.1 分页查询为什么总是重复数据如果你用了 ROWNUM 分页但没有最内层的 ORDER BY那么每次查询返回的行顺序可能不同就会出现第一页和第二页数据重复的现象。Oracle 不保证无 ORDER BY 时的返回顺序这跟 MySQL 还不完全一样。解决办法就是固定最内层的排序字段尽量选唯一性好的字段比如主键SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT * FROM books ORDER BY book_id ) t WHERE ROWNUM 20 ) WHERE rn 11;如果按书名排序而书名有重复那要在 ORDER BY 里加上主键做第二排序键保证翻页过程中数据顺序稳定。4.2 ORA-00942表或视图不存在这个错我见到过至少十次。排查思路就两条第一表是不是真的没建第二是不是连到了别的用户/Schema 下。Oracle 里的表是归属于某个用户的默认情况下你连接到 library 用户查询 readers 表没问题但如果你是 SYS 用户登录查 library.readers 不加 Schema 前缀就会报 ORA-00942。另一个常见原因是大小写Oracle 里用双引号创建的“小写表名”是例外情况正常创建的未加引号的表名都会转成大写。-- 正确不加引号让 Oracle 默认转大写 SELECT * FROM readers; -- 报错如果建表时用了双引号小写这里找不到 SELECT * FROM readers;所以建表语句里不要画蛇添足加双引号统一用不带引号的表名和字段名能少踩很多坑。4.3 ORA-01861字面值与格式字符串不匹配这个报错常见于往 DATE 字段插入字符串时格式不匹配。Oracle 默认日期格式一般是DD-MON-RR你传一个2025-01-10进去它就懵了。解决方式是在 Java 代码里用setDate方法ps.setDate(3, new java.sql.Date(System.currentTimeMillis()));不要在 SQL 里拼字符串日期。如果一定要用字符串用 TO_DATETO_DATE(2025-01-10, YYYY-MM-DD)这背后的逻辑就是Oracle 的隐式类型转换永远不如显式转换可靠。4.4 java.lang.OutOfMemoryError: insufficient memory这个报错通常不是 Java 堆内存不够而是系统进程的虚拟内存不足或连接数爆炸。在做图书馆系统时最典型的场景是你一次性把整个表的几万行数据全部加载进 List然后再分页渲染这当然会内存溢出。正确的思路是分页查询交给 SQLJava 端只保留当前页的数据。另外连接池如果不设置连接释放上限被异常中断的事务可能一直占着连接不归还积少成多也会把 Oracle 的进程数拖垮。排查方式登录 Oracle 查 v$session看哪些会话停留在 INACTIVE 状态过久。SELECT sid, serial#, username, status, machine, program FROM v$session WHERE username LIBRARY ORDER BY status;如果发现大量 INACTIVE 连接直接ALTER SYSTEM KILL SESSION sid,serial#然后回去检查连接池配置和代码里的 try-with-resources 用法。4.5 冷迁移换机器后怎么恢复数据库有人问过 Oracle 冷迁移的问题这虽然不是系统的日常功能但确实属于查错过程常见的运维场景。所谓冷迁移就是先把数据库正常关闭然后把数据文件、控制文件、重做日志文件整体复制到另一台机器最后在新环境重新打开。步骤大致是原机器执行SHUTDOWN IMMEDIATE或SHUTDOWN NORMAL。备份整个 ORACLE_HOME\oradata\实例名 目录。新机器安装同版本 Oracle然后把这些文件覆盖到对应目录。设置好环境变量和监听器用SQL STARTUP MOUNT打开实例。检查数据文件路径如果路径变了执行ALTER DATABASE RENAME FILE更新控制文件指向。ALTER DATABASE OPEN。这里很容易踩的坑是只备份了数据文件却漏了参数文件和密码文件结果启动时报错找不到 SPFILE。所以冷迁移时最好把 $ORACLE_HOME/dbs 下的参数文件也一起备份。4.6 报错排查速查表报错信息可能原因快速处理ORA-01017用户名或密码错误检查连接串用户密码ORA-12154TNS 无法解析检查监听和 tnsnames.oraORA-12514服务名监听失败确认连接的是 SID 还是服务名ORA-00942表或视图不存在检查 Schema 和大小写ORA-01861日期格式不匹配改用 TO_DATE 或 setDateORA-01858日期字段插入了字符串同上java.lang.ClassNotFoundException: oracle.jdbc.OracleDriver缺少 ojdbc 驱动 JAR把驱动包加入依赖或 WEB-INF/libjava.sql.SQLRecoverableException: IO Error网络不通或 Oracle 服务未启动ping 端口、检查监听最后分享一个我个人的操作习惯每写完一个 DAO 方法先停下来单独把 SQL 放到数据库客户端里跑一遍确认结果后再写 Java 代码。很多人喜欢一边写代码一边改 SQL最后分不清问题是出在 Java 层还是 SQL 层。先 SQL 后 Java 的流程能帮你把变量控制到最少。这套习惯让我调试这个项目的整体时间至少缩短了一半。本文还有配套的精品资源点击获取
返回列表