Java+Oracle图书馆管理系统:从表设计到事务分页的实战指南

📅 发布时间:2026/9/2 4:56:56
Java+Oracle图书馆管理系统:从表设计到事务分页的实战指南
简介这是一份面向Java初学者与数据库课程设计的图书馆管理系统源码包整合JavaOracle技术栈适合用于实训参考、毕业设计或课后练习。资源共98个文件主体为45个java源码与46个class编译文件可直接对照学习业务实现另附1个sql数据库脚本和db.properties配置方便在Oracle中快速建表与连接测试同时包含2个jar驱动包及项目配置文件整体压缩包5.54MB结构简洁、易于导入MyEclipse运行。已有1277人学习下载。内容围绕图书管理核心模块展开覆盖图书信息维护、读者管理、借阅与归还记录等场景涉及JDBC操作、SQL语句编写、控制台交互、异常处理等关键知识点。通过源码与数据库脚本的配合读者可直观理解从表结构设计到Java程序调用的完整链路尤其适合希望掌握传统JavaOracle开发流程、并独立完成小型信息管理系统的学习者。 做图书馆管理系统Java加Oracle这套组合放在今天看似乎有点“年代感”但真要说拿来练手、应付课程设计、夯实企业级开发基础它依然是相当扎实的一套搭配。尤其是Oracle在事务处理、存储过程、分页机制上与MySQL差异明显踩过一遍Oracle的坑再回去看MySQL会通透很多。这篇文章就把我在做这个“简单图书馆管理系统”过程中的设计思路、核心实现和踩坑记录完整捋一遍项目本身不难但里面每一处取舍都值得展开说说。1. 为什么这个“简单”系统偏偏选了Java和Oracle1.1 系统到底做了什么先把这个项目边界说清楚。虽然标题叫“简单图书馆管理系统”但它不是那种只有增删改查的玩具。实际做下来核心功能覆盖了这么几块图书信息管理录入、修改、下架、按书名/作者/分类模糊查询、读者管理办证、信息维护、状态管理、借书与还书流程含逾期天数计算、借阅历史查询以及简单的统计报表。整个系统基于Java SE JDBC或Java EE Servlet技术栈都能跑界面可以是控制台、Swing也可以套个Web壳我用的是传统Servlet JSP那套便于把精力放在业务本身。这个项目适合谁来参考三类人一个是正在做课程设计或毕业设计的学生整套代码和表结构可以直接借鉴一个是准备Java面试的开发者里面涉及JDBC、事务、连接池、分页、存储过程全是常考内容还有一个是想体验Oracle和MySQL差异的初级工程师用一个小项目迁移一遍比看书印象深得多。1.2 选Oracle而不是MySQL差别在哪很多人一上来就问为什么要用OracleMySQL不行吗当然行但既然标题定的是JavaOracle这个选型本身就是有讲究的。首先Oracle在数据的严谨性上要求更高。比如空字符串和NULL是两回事VARCHAR2的空串会被当作NULL处理这个在MySQL里完全不是同一个逻辑。你在Oracle里写WHERE name 查不出任何东西必须用IS NULL这种细节写代码时非常容易翻车。其次Oracle的分页方式是一大经典考点。MySQL用LIMITOracle早期用ROWNUM后来推荐用ROW_NUMBER()窗口函数。你面试时如果说只会MySQL的LIMIT面试官大概率会追问Oracle怎么分页这就是这套系统能给你补上的知识盲区。还有一点是事务和锁的机制。Oracle的默认隔离级别是READ COMMITTEDMVCC实现和InnoDB思路类似但细节不同。在并发借书、还书场景下如何处理行锁、避免脏读这套系统会让你实实在在地操作一遍而不是停留在理论。如果你问我最直观的体会做完这个项目再去理解执行计划、索引失效、事务隔离级别这些概念会有一种“原来是这么回事”的感觉。Oracle的SQL执行计划和AWR报告虽然复杂但小系统已经能暴露不少性能问题足够练手了。2. 数据建模Oracle下的表结构设计与主键玩法2.1 三张核心表定下了整个系统的骨架图书馆管理系统的核心数据模型绕不开三张表图书表BOOK、读者表READER、借阅表BORROW_RECORD。设计时每张表字段的取舍直接决定了后面代码的复杂度我最初设计时反复改过几版这里给出我认为最稳的一版结构。图书表BOOK字段包括BOOK_ID主键、BOOK_NAME书名、AUTHOR作者、PUBLISHER出版社、ISBN国际标准书号、CATEGORY_ID分类ID、STOCK_TOTAL总库存、STOCK_AVAILABLE可借库存、LOCATION馆藏位置、CREATE_TIME录入时间。其中STOCK_AVAILABLE这个字段很关键它在借书时减一、还书时加一是控制并发借阅的核心。读者表READERREADER_ID主键、READER_NAME姓名、ID_CARD身份证号、PHONE手机号、REG_DATE办证日期、STATUS状态1正常/0冻结。这里ID_CARD和PHONE最好加唯一约束否则同一个读者反复办证会导致数据冗余。借阅表BORROW_RECORDRECORD_ID主键、BOOK_ID外键、READER_ID外键、BORROW_DATE借书日期、DUE_DATE应还日期、RETURN_DATE实际归还日期未还则为NULL、STATUS状态借出中/已归还/逾期。因为要算逾期天数DUE_DATE是个必要的冗余字段可以通过BORROW_DATE加固定天数算出但落库存储后查询时直接用即可不用每次计算。2.2 主键生成序列加触发器才是Oracle的经典姿势Oracle和MySQL一个非常大的不同在于主键自增。MySQL可以用AUTO_INCREMENTSQL Server有IDENTITY而Oracle 12c之前没有自增列的概念主键一般靠序列SEQUENCE加触发器TRIGGER配合实现。这在面试中属于基础题但很多从MySQL转过来的同学第一次接触会懵。-- 创建序列 CREATE SEQUENCE SEQ_BOOK_ID START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE; -- 创建触发器插入时自动取序列值作为主键 CREATE OR REPLACE TRIGGER TRG_BOOK_ID BEFORE INSERT ON BOOK FOR EACH ROW BEGIN SELECT SEQ_BOOK_ID.NEXTVAL INTO :NEW.BOOK_ID FROM DUAL; END; /这里有一个细节值得展开NOCACHE和CACHE的区别。如果使用CACHE 20Oracle会预先生成20个序列值放在内存里性能更好但如果数据库突然宕机这20个值会被跳过主键出现断层不影响业务但会有“插入后主键不是连续递增”的情况。对于图书馆这种低频写入系统NOCACHE完全足够性能差异可以忽略。另外很多教材喜欢用SYS_GUID()生成UUID作为主键这虽然避免了序列的麻烦但主键是32位字符串占用空间大索引效率也更差。业务上图书编号用纯数字序列更友好所以我最终采用序列方案。2.3 分类表的层级结构CONNECT BY的用武之地图书分类是个容易被低估的设计。如果你只存一个分类名称字符串后面要做“所有计算机类图书”“所有文学类图书”这种统计时就很痛苦因为分类是天然有层级的。比如“计算机”下面有“编程语言”“数据库”“操作系统”三级目录查询时你需要把子分类全部带出来。Oracle的CONNECT BY语法在这里非常合适。先建一张CATEGORY表CREATE TABLE CATEGORY ( CATEGORY_ID NUMBER PRIMARY KEY, PARENT_ID NUMBER, CATEGORY_NAME VARCHAR2(50) ); INSERT INTO CATEGORY VALUES (1, NULL, 计算机); INSERT INTO CATEGORY VALUES (2, 1, 编程语言); INSERT INTO CATEGORY VALUES (3, 1, 数据库); INSERT INTO CATEGORY VALUES (4, 2, Java); INSERT INTO CATEGORY VALUES (5, 3, Oracle);要查“计算机”分类下所有子分类的ID只需要一句SELECT CATEGORY_ID FROM CATEGORY START WITH CATEGORY_ID 1 CONNECT BY PRIOR CATEGORY_ID PARENT_ID;这个写法比递归CTE简洁得多也是Oracle程序员比较得意的语法点。系统在“按分类浏览图书”功能里用到了这个查询把取到的所有子分类ID传给IN条件一次就能把某个大类下的全部图书捞出来。这个语法在面试里出现频率不高但属于加分项掌握了写在简历上很亮眼。3. 数据访问层JDBC到连接池的选型心路3.1 ojdbc驱动和连接串是第一个拦路虎Java连Oracle第一步就不是特别顺。Oracle的JDBC驱动版本众多ojdbc14、ojdbc6、ojdbc8、ojdbc11不同版本对应不同JDK。很多初学者把驱动jar包一股脑塞进项目然后报各种ClassNotFoundException或Unsupported major.minor version其实都是版本和JDK不匹配。目前的稳定选择是JDK 8用ojdbc8JDK 11及以上可以用ojdbc11。驱动包可以直接从Oracle官网下载或者如果用的是MavenOracle驱动比较特殊中央仓库没有官方版需要手动把jar包安装到本地仓库mvn install:install-file -Dfileojdbc8.jar -DgroupIdcom.oracle.database.jdbc -DartifactIdojdbc8 -Dversion19.8.0.0 -Dpackagingjar连接串也是一个经典坑点。Oracle的连接串有两种写法SID模式和服务名模式区别很细微// SID模式老写法 jdbc:oracle:thin://localhost:1521:orcl // 服务名模式推荐 jdbc:oracle:thin://localhost:1521/orclSID模式用的是冒号服务名模式用的是斜杠。本地安装的Oracle默认有一个SID叫orcl但如果是连接Oracle容器数据库CDB的PDB比如自建了一个名为PDBORCL的可插拔数据库就必须用服务名模式。搞不清这两者区别连接报错时排查半天毫无头绪是Oracle新手常见的噩梦之一。3.2 用连接池别用DriverManager如果是做个几百行的命令行Demo直接用DriverManager获取连接没问题。但一旦涉及Web系统或者并发访问一定要用连接池。Java自带的DriverManager每次请求都会创建物理连接Oracle的连接建立非常昂贵高并发下数据库很快会被打挂。我用的是阿里巴巴的Druid连接池选它的原因很简单监控能力强自带SQL防注入检测对Oracle兼容性好。核心配置如下DruidDataSource dataSource new DruidDataSource(); dataSource.setDriverClassName(oracle.jdbc.OracleDriver); dataSource.setUrl(jdbc:oracle:thin://localhost:1521/orcl); dataSource.setUsername(library); dataSource.setPassword(library123); dataSource.setInitialSize(5); dataSource.setMinIdle(5); dataSource.setMaxActive(20); dataSource.setValidationQuery(SELECT 1 FROM DUAL);注意那个validationQueryOracle里必须写SELECT 1 FROM DUAL写成SELECT 1会直接报ORA-00923错误因为Oracle不像MySQL那样允许没有FROM的SELECT。这个是Oracle和MySQL的差异点面试中偶尔也会被问到。3.3 PreparedStatement防注入不是说着玩的做数据访问层时很多人知道PreparedStatement能防SQL注入但说不出原理。JDBC的PreparedStatement会在数据库端对SQL进行预编译参数与SQL语句分离开来用户输入的内容只会被当作参数值处理不会被拼进SQL结构里。而Statement直接拼接字符串一旦输入 OR 11这样的内容SQL语义就会被彻底改写。// 正确写法 String sql SELECT * FROM BOOK WHERE BOOK_NAME LIKE ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, % keyword %); ResultSet rs ps.executeQuery();有个小坑要提醒Oracle的LIKE查询遇到关键字本身包含百分号或下划线时默认会当通配符处理。如果图书名里真有这些字符需要转义处理。我当初做模糊搜索时没注意用户搜“100%”居然把很多书都带出来了。后来在SQL里加了ESCAPE子句才根治SELECT * FROM BOOK WHERE BOOK_NAME LIKE % || ? || % ESCAPE \\代码里再对关键字中的%和_做转义这个细节虽然不影响系统运行但数据真的脏起来时再回头改就麻烦了。4. 借还业务与分页查询的实现细节4.1 借书流程里的事务边界要画清楚借书这个操作看起来很直白但其实包含多个数据库操作查询图书在库状态、查询读者状态、插入借阅记录、更新图书可借库存。任何一个步骤失败前面的操作都得回滚否则会出现“借阅记录插了但库存没减”这种数据不一致。Connection conn dataSource.getConnection(); try { conn.setAutoCommit(false); // 1. 查询图书库存加行锁 String sqlCheck SELECT STOCK_AVAILABLE FROM BOOK WHERE BOOK_ID ? FOR UPDATE; // 2. 判断库存 0否则抛异常 // 3. 插入借阅记录 String sqlInsert INSERT INTO BORROW_RECORD (BOOK_ID, READER_ID, BORROW_DATE, DUE_DATE) VALUES (?, ?, ?, ?); // 4. 更新库存 String sqlUpdate UPDATE BOOK SET STOCK_AVAILABLE STOCK_AVAILABLE - 1 WHERE BOOK_ID ?; conn.commit(); } catch (Exception e) { conn.rollback(); } finally { conn.setAutoCommit(true); conn.close(); }其中SELECT ... FOR UPDATE是重点。它会对这条图书记录加行级排他锁其他会话在修改这条记录时必须等待这样两个用户同时借同一本书的最后一本时后一个事务会阻塞第一个事务提交后第二个事务才会继续执行此时再查库存就是0了自然就走到了“库存不足”的分支避免超借。不加这个锁两个事务同时读到库存为1都执行库存减一最后库存会变成-1这就是经典的并发丢失更新问题。4.2 还书和逾期计算TRUNC(SYSDATE)的妙用还书逻辑比借书简单但逾期天数的计算是个必须做好的点。思路是还书时取当前时间SYSDATE和借阅记录的应还日期DUE_DATE做减法。注意Oracle里日期相减得到的是天数的小数直接减可能带小数。SELECT TRUNC(SYSDATE) - TRUNC(DUE_DATE) AS OVERDUE_DAYS FROM BORROW_RECORD WHERE RECORD_ID ?;这里为什么用TRUNC(SYSDATE)而不是SYSDATE因为SYSDATE包含时分秒今天是20点还书应还日期是昨天如果直接相减会得到1.8天而不是1天统计逾期天数时就会多算一天。TRUNC函数会把时间部分截断只留日期这样计算结果才是纯粹的整数天数。这个细节我一开始没注意测试时无论什么时候还书逾期天数都带上小数打印出来特别不专业后来才加上TRUNC修正。还书操作同时要更新两条数据一是把BORROW_RECORD表里的RETURN_DATE和STATUS更新二是把BOOK表里的STOCK_AVAILABLE加一。同样包在事务里执行。如果系统里有“逾期罚款”的需求还可以再加一张罚款表和一条计算逻辑不过我的系统里暂没做已经够用。4.3 Oracle分页的两种写法与存储过程之争图书列表和借阅历史查询必然要分页。Oracle分页最经典的方式是ROWNUM但ROWNUM有个特点它是在结果集生成前分配的伪列必须在子查询里先排序再在外面过滤否则分页会乱。-- ROWNUM分页 SELECT * FROM ( SELECT A.*, ROWNUM RN FROM (SELECT * FROM BOOK ORDER BY BOOK_ID) A WHERE ROWNUM 20 ) WHERE RN 10;这写法现在已经不推荐了更好的方式是用分析函数SELECT * FROM ( SELECT B.*, ROW_NUMBER() OVER (ORDER BY BOOK_ID) AS RN FROM BOOK B ) WHERE RN BETWEEN 11 AND 20;ROW_NUMBER() OVER这种写法可读性更强而且便于扩展排序条件。两种方案的性能在半斤八两之间但在Oracle 12c以后还有OFFSET分页写法类似LIMIT ? OFFSET ?但大量数据下性能不一定更好。我最终选用ROWNUM封装成工具方法因为它在老版本Oracle里兼容性最好。关于存储过程一开始我把借书逻辑封装成了存储过程PROC_BORROW_BOOK想着Java端只要一行CallableStatement调用就完事事务都在数据库里处理很“企业级”。后来发现一个小问题存储过程里的COMMIT和Java代码里的事务控制是割裂的如果存储过程里提交了Java端就无法再回滚整个事务如果Java端统一控制存储过程里就不能写COMMIT。这个协调很费精力。对小系统来说事务控制在Service层用Java代码管理更灵活存储过程在复杂报表统计时用一下就够了业务逻辑我最后还是搬回了Java。5. 集成Oracle绕不开的经典坑5.1 中文乱码NLS_LANG和字符集必须对齐如果哪次连接Oracle没遇到中文乱码那运气真的不错。乱码的根源是客户端和数据库端字符集不一致。查看数据库字符集的SQLSELECT USERENV(LANGUAGE) FROM DUAL;会看到类似SIMPLIFIED CHINESE_CHINA.AL32UTF8或SIMPLIFIED CHINESE_CHINA.ZHS16GBK的结果。如果你程序里用的是UTF-8而数据库是ZHS16GBK插入中文就会变问号。解决方案是保持两端一致要么建库时选AL32UTF8推荐要么在JDBC连接串里加上?useUnicodetruecharacterEncodingUTF-8但Oracle对连接串参数支持不如MySQL真正彻底的解决还是统一库的字符集。还有一个环境变量NLS_LANG在Windows环境变量里设置NLS_LANGSIMPLIFIED CHINESE_CHINA.AL32UTF8能解决一些SQLPlus导入乱码问题。我在Windows下用SQLPlus执行建表脚本时经常遇到中文变成乱码的情况设置了这个环境变量后就没再出现。5.2 JDK版本和“源发行版”警告用IDEA或Eclipse运行项目时控制台偶尔会打印一条java: 警告: 源发行版 17 需要目标发行版 17这个报错的意思是当前项目编译级别设置为17但IDE的Java编译器运行版本低于17或者项目里某些依赖是用17编译的。项目里如果混用不同JDK版本就会出现这种问题。解决办法是检查Project Structure里的SDK版本和Language Level保持一致。我在迁移到OpenJDK环境时遇到过改了Language Level后就好了属于小问题但很影响开发体验。另外Oracle官方JDK和OpenJDK兼容性很好如果在意版权或成本问题直接换Dragonwell或OpenJDK完全不影响本项目的运行。5.3 数据库迁移冷迁移与数据泵的取舍换了电脑或服务器之后数据怎么搬这里有个热词叫“Oracle 11g数据库冷迁移”。冷迁移的核心思路是停库后直接拷贝数据文件简单粗暴但对文件一致性要求很高如果你没有严格按照步骤执行先干净关闭数据库拷过去的数据文件是损坏的起不来。谨慎起见我推荐用Oracle自带的逻辑备份工具expdp和impdp迁移思路清晰得多# 在源库执行导出 expdp library/library123orcl schemaslibrary directoryDATA_PUMP_DIR dumpfilelibrary.dmp logfilelibrary.log # 在目标库执行导入 impdp library/library123orcl schemaslibrary directoryDATA_PUMP_DIR dumpfilelibrary.dmp logfilelibrary_imp.log这套流程里有几个隐性要求源库和目标库的字符集最好一致导入前先建好用户和表空间如果目标库已有同名表要加TABLE_EXISTS_ACTIONREPLACE参数。我迁移过程中遇到过ORA-31684错误就是目标库已经存在同名对象导致的加上参数后重新导入就好了。冷迁移我建议只在数据量极大、停机窗口充足的情况下使用小项目用expdp完全够用。6. 性能优化与安全体检的实战手记6.1 索引设计决定查询的命脉系统数据量小的时候怎么查都快但一旦数据量上来索引设计就显形了。我的原则是外键列必须建索引高频查询条件必须建索引。-- 外键字段索引 CREATE INDEX IDX_BORROW_BOOK_ID ON BORROW_RECORD(BOOK_ID); CREATE INDEX IDX_BORROW_READER_ID ON BORROW_RECORD(READER_ID); -- 图书查询常用字段 CREATE INDEX IDX_BOOK_NAME ON BOOK(BOOK_NAME); CREATE INDEX IDX_BOOK_ISBN ON BOOK(ISBN);注意一点LIKE %关键字%这种前置模糊查询会走全表扫描即使字段上有索引也用不上这是Oracle的索引机制决定的。如果图书列表查询非常频繁可以考虑Oracle的全文索引或CONTEXT类型索引但对这个项目来说有点大炮打蚊子了。真正能做的优化是尽量让搜索字段能用B树索引的等值或范围查询模糊搜索在数据量可控时容忍全表扫描。验证索引是否生效最直接的方式是看执行计划。用EXPLAIN PLAN FOR加DBMS_XPLAN.DISPLAY查看EXPLAIN PLAN FOR SELECT * FROM BOOK WHERE BOOK_NAME Java核心技术; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);执行计划里如果看到TABLE ACCESS FULL说明没走索引如果看到INDEX RANGE SCAN说明索引生效了。这个查看习惯一定要养成别等线上卡了才开始学。6.2 数据库安全体检的几个基本动作做系统时顺手做了一轮数据库安全方面的检查虽然是个小Demo但这些意识值得保留。基本动作包括确认监听器没有暴露到公网、业务账号和DBA账号分离、定期审计日志开启。Oracle默认开启了审计功能可以查看DBA_AUDIT_TRAIL这张表。如果没开启可以用ALTER SYSTEM SET audit_trailDB SCOPESPFILE;另外检查账号的密码策略和登录失败次数限制Oracle自带DEFAULT密码配置文件可以设置FAILED_LOGIN_ATTEMPTS来防止暴力破解。这类操作不涉及具体合规政策纯粹是从数据库安全加固角度出发的常规操作建议每个做Oracle的人都了解一下。6.3 并发借书时的行锁等待一个需要预判的隐患最后分享一个我自己在实际测试中踩到的坑。当时用两个窗口模拟并发借书第二个窗口卡住不动了我等了十几秒开始怀疑程序死循环。其实不是是第一个事务还没提交行级锁没释放第二个事务在等待锁。Oracle里可以通过以下SQL查看锁等待情况SELECT sid, serial#, username, blocking_session, event FROM v$session WHERE blocking_session IS NOT NULL;找到被阻塞的会话后如果确实需要强制结束可以执行ALTER SYSTEM KILL SESSION sid,serial#。但注意这条命令比较暴力生产环境慎用。要避免这种问题一是尽量缩短事务时间借书操作里的查询、插入、更新要快不要在事务里执行耗时的外部接口调用或批量计算二是合理安排锁的顺序所有客户端都以相同的顺序访问表减少死锁概率。图书馆管理系统的并发量不大但理清这层逻辑后对理解Oracle的锁管理机制很有帮助面试聊到并发控制时也能多几句深入的内容。这套系统从表结构设计到JDBC编码再到性能优化整体走下来最大的感受是技术栈老归老但正因为老它的每个环节都有大量现场经验可以学比直接用框架自动生成要扎实得多。如果你也在用Java配合Oracle做类似的系统遇到某个具体报错卡住的时候随时回来对照着排查一遍大概率能省下不少折腾的时间。本文还有配套的精品资源点击获取