十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Oracle图书管理系统数据库设计实战:从建表到排错

Oracle图书管理系统数据库设计实战:从建表到排错 简介这是一份面向数据库课程设计与毕业设计的Oracle图书管理系统数据库设计文档适合高校学生、初级DBA以及需要完成同类课设的开发者参考。文档以完整的课设论文结构展开系统梳理了需求分析、设计目标、项目规划、系统功能模块划分并重点讲解数据库概念结构、逻辑结构与物理结构设计覆盖表空间、数据表、视图、序列、索引、存储过程及触发器的创建与管理同时给出数据查询、数据更新、数据合并等访问示例内容由浅入深便于读者对照练习。资源为单个Word文档约319KB文件虽小但章节体系完整兼具设计思路、建表脚本与操作方法说明可作为课程设计说明书、实验报告或答辩演示的蓝本。目前已有281人学习浏览适合作为数据库设计入门和Oracle实践项目的参考资料。1. 一份.doc文档背后Oracle图书管理系统的真正开头很多人拿到《Oracle图书管理系统数据库设计与实现.doc》第一反应是找里面的建表语句。但真正做过Oracle开发的人知道这份文档的价值不在那几十条DDL而在它逼迫你先把课题边界定清楚图书管理系统看起来是图书、读者、借阅三个词可一旦落到Oracle你立刻要回答三个问题——表之间用物理外键还是逻辑外键借还书用存储过程还是三层架构里拼SQL分页用ROWNUM还是分析函数。这三个问题都不会写进课程设计的.doc里但它们决定系统是能跑完答辩还是能跑上生产。这篇文章直接拿Oracle数据库说事按“设计→建库→业务→排错→文档化”的顺序把一份文档能展开成什么样讲透。适合正在做毕业设计、课程设计或者刚接手一个Oracle老系统的开发人员读完你至少能照着建出能跑的库并知道每个参数为什么这么设。2. 图书管理系统的ER到表结构反范式设计前的三个决定2.1 需求边界图书信息之外还要不要管读者模型图书管理系统最常见的错误是一上来就画图书和借阅两张表。可仔细想想借阅记录里要存读者姓名还是读者编号如果存姓名读者改名怎么办如果存编号那读者表就必须有唯一约束。更进一步一本书可能有多个副本书名一样但条码不同一个读者可能同时借多本书逾期还要按天算罚款。这些需求不写进ER图之后改表结构会非常痛苦。我一般先按最小可用集设计图书表含条形码唯一标识、读者表含证件号、状态、借阅表一次借阅动作形成一行。存罚款金额时会冗余一个“应还时间”字段而不是每次都用当前时间减借出时间去算——虽然这违反一点第三范式但能避免在查询里反复做日期运算。标题里的.doc如果只有三个表那它多半停留在课程层面真实系统至少还要有“图书分类表”和“管理员操作日志表”。本系统以借阅为核心所以后文聚焦三张核心表。2.2 核心表图书表、读者表、借阅单表与状态机图书表的主键我不爱用自增而是用“条码号VARCHAR2”因为实体书的条码是物理存在扫描枪扫进来的就是字符串。如果系统里允许两本一模一样的书那么书名加作者根本无法区分必须靠条码。读者表主键可以用ID NUMBER通过序列生成也可以用“借书证号”这个取决于学校或单位是否已有规则。借阅表主键建议用“借阅ID”关联图书条码和读者ID同时记录借出时间、应还时间、实际归还时间、状态。状态字段是整个设计的灵魂。我会用它维护一个状态机状态为“借出”时实际归还时间为空状态为“已还”时实际归还时间非空状态为“逾期”时表示当前时间大于应还时间且未归还。不用让程序到处判断日期而是让状态列有CHECK约束并保证只有合法迁移。下面这张字段设计表是建库前的最终口径。表名字段类型约束/说明BOOK图书表BARCODEVARCHAR2(20)PK物理条码TITLEVARCHAR2(200)NOT NULL书名AUTHORVARCHAR2(100)作者CATEGORY_IDNUMBERFK到分类表STATUSCHAR(1)1在馆0借出READER读者表READER_IDNUMBERPK序列生成READER_NOVARCHAR2(20)UNIQUE证号NAMEVARCHAR2(50)NOT NULLSTATUSCHAR(1)1正常0挂失BORROW借阅表BORROW_IDNUMBERPKBARCODEVARCHAR2(20)FK到BOOKREADER_IDNUMBERFK到READERBORROW_DATEDATE默认SYSDATEDUE_DATEDATE借出60天RETURN_DATEDATE空表示未还STATUSCHAR(1)0借出1已还2逾期2.3 用CHECK约束和主外键锁死业务规则很多从MySQL过来的人不喜欢在数据库层做约束觉得应用层校验就够了。但Oracle的数据库是给多程序共用的——可能有Java后台、管理端脚本、报表系统同时连线约束放在数据库里才是唯一靠得住的。我会在借阅表的STATUS上加CHECK在RETURN_DATE上做逻辑约束状态为已还时RETURN_DATE不能为空。虽然CHECK约束不能直接引用另一行但可以用复合条件实现ALTER TABLE BORROW ADD CONSTRAINT CK_BORROW_STATUS CHECK (STATUS IN (0,1,2)); -- 如果已还则必须存在归还日期借出状态下允许为空 ALTER TABLE BORROW ADD CONSTRAINT CK_BORROW_RETURN CHECK (STATUS ! 1 OR RETURN_DATE IS NOT NULL);第一个CHECK很简单就是枚举状态。第二个CHECK用了蕴含逻辑STATUS不等于1时整个条件为TRUE等于1时就变成“RETURN_DATE IS NOT NULL”必须成立。这是Oracle里做跨字段约束最常见的手法。设计文档里如果能画出带状态约束的ER图比光画几个椭圆框更有说服力。外键方面我建议BARCODE、READER_ID都建物理外键但要在对应列上建索引否则删除父表时Oracle会全表扫描子表数据量一大就慢。3. Oracle建库与用户授权先建表空间再谈表3.1 创建用户与表空间的SQL模板拿到一台装了Oracle 11g或12c的服务器先别急着CREATE TABLE。第一步是规划表空间和数据文件。图书管理系统不算重负载但至少要区分“图书数据”和“索引数据”两个表空间否则后期索引膨胀会影响备份恢复。我一般用一个数据文件放表一个数据文件放索引并设置区段管理为本地管理、段空间管理为自动。-- 表空间BOOK_DATA CREATE TABLESPACE BOOK_DATA DATAFILE /u01/app/oracle/oradata/ORCL/book_data01.dbf SIZE 500M AUTOEXTEND ON NEXT 50M MAXSIZE 2048M LOGGING EXTENT MANAGEMENT LOCAL SEGMENT SPACE MANAGEMENT AUTO; -- 索引表空间BOOK_IDX CREATE TABLESPACE BOOK_IDX DATAFILE /u01/app/oracle/oradata/ORCL/book_idx01.dbf SIZE 200M AUTOEXTEND ON NEXT 50M MAXSIZE 1024M EXTENT MANAGEMENT LOCAL SEGMENT SPACE MANAGEMENT AUTO; -- 创建用户并授权 CREATE USER LIBRARY IDENTIFIED BY Lib_2024 DEFAULT TABLESPACE BOOK_DATA QUOTA UNLIMITED ON BOOK_DATA QUOTA UNLIMITED ON BOOK_IDX; GRANT CONNECT, RESOURCE TO LIBRARY; GRANT CREATE VIEW, CREATE SYNONYM TO LIBRARY;数据文件的路径要提前确认不同机器上ORACLE_BASE和ORACLE_SID不一样直接抄网上的绝对路径容易启动报错。AUTOEXTEND ON是给课程设计用的生产环境一般关闭自动扩展或设一个很大的MAXSIZE防止文件在磁盘上碎掉。RESOURCE角色在Oracle 11g里已经绑定UNLIMITED TABLESPACE了吗并不所以要单独给用户授权两个表空间的配额否则建表会报ORA-01950。3.2 能重复执行的建表脚本PL/SQL判断开发和答辩过程中表结构往往会改几次每次手动删表重建很容易误删数据。我习惯把整个结构写成可重复执行的脚本用PL/SQL提前判断对象是否存在存在则先DROP再CREATE。这个技巧尤其适合放在.doc的附录里考官一看就知道你对Oracle的DDL执行机制有概念。DECLARE cnt NUMBER; BEGIN SELECT COUNT(*) INTO cnt FROM USER_TABLES WHERE TABLE_NAME BORROW; IF cnt 0 THEN EXECUTE IMMEDIATE DROP TABLE BORROW CASCADE CONSTRAINTS; END IF; -- 同样检查 BOOK、READER END; / -- 按依赖顺序创建分类表 - 图书表 - 读者表 - 借阅表 CREATE TABLE BOOK ( BARCODE VARCHAR2(20) NOT NULL ,TITLE VARCHAR2(200) NOT NULL ,AUTHOR VARCHAR2(100) ,CATEGORY_ID NUMBER ,STATUS CHAR(1) DEFAULT 1 ,CONSTRAINT PK_BOOK PRIMARY KEY (BARCODE) USING INDEX TABLESPACE BOOK_IDX ) TABLESPACE BOOK_DATA; CREATE TABLE READER ( READER_ID NUMBER NOT NULL ,READER_NO VARCHAR2(20) NOT NULL ,NAME VARCHAR2(50) NOT NULL ,STATUS CHAR(1) DEFAULT 1 ,CONSTRAINT PK_READER PRIMARY KEY (READER_ID) USING INDEX TABLESPACE BOOK_IDX ,CONSTRAINT UK_READER_NO UNIQUE (READER_NO) ) TABLESPACE BOOK_DATA; CREATE TABLE BORROW ( BORROW_ID NUMBER NOT NULL ,BARCODE VARCHAR2(20) NOT NULL ,READER_ID NUMBER NOT NULL ,BORROW_DATE DATE DEFAULT SYSDATE ,DUE_DATE DATE DEFAULT SYSDATE 60 ,RETURN_DATE DATE ,STATUS CHAR(1) DEFAULT 0 ,CONSTRAINT PK_BORROW PRIMARY KEY (BORROW_ID) USING INDEX TABLESPACE BOOK_IDX ,CONSTRAINT FK_BORROW_BOOK FOREIGN KEY (BARCODE) REFERENCES BOOK (BARCODE) ,CONSTRAINT FK_BORROW_READER FOREIGN KEY (READER_ID) REFERENCES READER (READER_ID) ) TABLESPACE BOOK_DATA;CASCADE CONSTRAINTS很关键不写它有外键依赖时DROP会报ORA-02449。USING INDEX TABLESPACE BOOK_IDX是让主键索引落到索引表空间这是一种常用优化否则默认和表放一起。另外注意VARCHAR2在Oracle里按字节存中文最大长度要预留3倍VARCHAR2(200)可以放66个汉字多数书名够用。3.3 序列、触发器和索引的选择主键我建议用序列不用IDENTITY列因为Oracle 11g不支持12c之前的版本都得用序列加触发器模拟自增。如果你用Oracle 12c可以直接用IDENTITY但考虑到大部分课程设计还在用11g序列是最兼容的写法。CREATE SEQUENCE SEQ_READER_ID START WITH 10001 INCREMENT BY 1 NOCACHE; CREATE SEQUENCE SEQ_BORROW_ID START WITH 1 INCREMENT BY 1 NOCACHE; CREATE OR REPLACE TRIGGER TRI_READER_ID BEFORE INSERT ON READER 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; /触发器里判断IF :NEW.READER_ID IS NULL是为了兼容手动指定主键的导入场景。NOCACHE对两个低频表够用同时也避免了RAC环境下序列断层。索引方面外键列上必须建索引否则删除父表时Oracle会做锁和全表扫描这也是Oracle与MySQL一个显著区别MySQL InnoDB外键默认不加索引时也会自动建但Oracle不会。用一条语句把借阅表上的三个外键列建好复合索引CREATE INDEX IDX_BORROW_REL ON BORROW (READER_ID, BARCODE) TABLESPACE BOOK_IDX;复合索引列顺序按“查询中WHERE过滤最频繁的列放前面”来定这里READER_ID查一个人借了哪些书用得多所以放前面。至于TITLE字段经常做模糊查询不用建常规B树索引等数据量大了再考虑Oracle Text或函数索引。4. 借阅流程的存储过程与分页实现4.1 借书与还书的两个存储过程事务控制图书管理系统的核心不是建表而是借书、还书这个事务。借书至少要做两件事往BORROW插一行同时把BOOK.STATUS改成0。如果不用存储过程Java里连着发两条UPDATE/INSERT中间一旦断电就会出现“书被借走但记录没建”。把两条DML包进一个存储过程由Oracle来保证原子性是最可靠的做法。CREATE OR REPLACE PROCEDURE SP_BORROW( p_barcode IN VARCHAR2, p_reader_id IN NUMBER ) AS v_status CHAR(1); BEGIN -- 锁定图书行防止同一本书并发借出 SELECT STATUS INTO v_status FROM BOOK WHERE BARCODE p_barcode FOR UPDATE; IF v_status 0 THEN RAISE_APPLICATION_ERROR(-20001, Book is already borrowed); END IF; INSERT INTO BORROW(BORROW_ID, BARCODE, READER_ID, BORROW_DATE, DUE_DATE, STATUS) VALUES(SEQ_BORROW_ID.NEXTVAL, p_barcode, p_reader_id, SYSDATE, SYSDATE60, 0); UPDATE BOOK SET STATUS 0 WHERE BARCODE p_barcode; COMMIT; EXCEPTION WHEN OTHERS THEN ROLLBACK; RAISE; END SP_BORROW; /FOR UPDATE是并发控制的要点。不加它两个会话同时执行这个过程都可能读到STATUS1然后都插入借阅记录书只有一本却借给两个人。加锁后第二个会话会阻塞在SELECT上等第一个事务提交后再读到0进而触发RAISE_APPLICATION_ERROR。返回给应用层的错误码-20001能直接被Java捕获。注意异常处理里先ROLLBACK再RAISE否则Oracle会保留未提交的数据状态应用层感知不到失败。还书的过程类似要计算是否逾期逾期时直接更新状态为2还是记录罚款金额由需求决定。这里给出一个最小版本CREATE OR REPLACE PROCEDURE SP_RETURN( p_barcode IN VARCHAR2, p_reader_id IN NUMBER ) AS v_due_date DATE; BEGIN SELECT DUE_DATE INTO v_due_date FROM BORROW WHERE BARCODE p_barcode AND READER_ID p_reader_id AND RETURN_DATE IS NULL; UPDATE BORROW SET RETURN_DATE SYSDATE, STATUS CASE WHEN SYSDATE v_due_date THEN 2 ELSE 1 END WHERE BARCODE p_barcode AND READER_ID p_reader_id AND RETURN_DATE IS NULL; UPDATE BOOK SET STATUS 1 WHERE BARCODE p_barcode; COMMIT; EXCEPTION WHEN NO_DATA_FOUND THEN RAISE_APPLICATION_ERROR(-20002, Borrow record not found); END SP_RETURN; /这里用了CASE WHEN做状态迁移比在Java里写两个UPDATE要清晰。注意SELECT DUE_DATE INTO如果查不到记录会触发NO_DATA_FOUND用RAISE_APPLICATION_ERROR转换成业务异常避免握着一个ORA-01403往外传。还书时没有用FOR UPDATE定位借阅记录因为这里借阅记录不是更新数量而是更新状态丢失更新问题通过RETURN_DATE IS NULL条件来兜底并发双还时第二个会话更新0行仍能正常返回只是违背业务预期——所以生产环境还是建议加上FOR UPDATE。4.2 热门图书TOP榜用ROWNUM还是FETCH FIRST图书管理系统免不了“最热图书”这类榜单。Oracle分页最大的坑是ROWNUM不能直接和ORDER BY一起用必须嵌套子查询。很多新手写WHERE ROWNUM 10 ORDER BY borrow_count DESC得到的是排序前的10条纯属错误。正确写法是先把结果排序再套一层ROWNUMSELECT * FROM ( SELECT b.BARCODE, b.TITLE, COUNT(*) AS BORROW_CNT FROM BORROW br JOIN BOOK b ON b.BARCODE br.BARCODE GROUP BY b.BARCODE, b.TITLE ORDER BY BORROW_CNT DESC ) WHERE ROWNUM 10;内层完成聚合和排序外层ROWNUM取前10。ROWNUM必须在最外层过滤这是Oracle 11g及以下版本的标准分页姿势也是面试常问的题目。如果你用的是Oracle 12c可以写成FETCH FIRST 10 ROWS ONLY更直观SELECT b.BARCODE, b.TITLE, COUNT(*) AS BORROW_CNT FROM BORROW br JOIN BOOK b ON b.BARCODE br.BARCODE GROUP BY b.BARCODE, b.TITLE ORDER BY BORROW_CNT DESC FETCH FIRST 10 ROWS ONLY;老系统里ROWNUM方式还是要懂因为很多生产环境还跑在11g上。且FETCH FIRST在11g会直接语法报错版本判断不能只看字符集。4.3 记录状态用CASE WHEN日期精确到天用TRUNC业务列表页需要显示“在馆/借出/逾期”而不是0/1/2直接用Oracle的CASE WHEN表达式做翻译省得在Java里循环判断。日期字段因为带着时分秒按天统计时容易把同一天的数据漏掉处理办法是TRUNC掉时间部分。SELECT br.BORROW_ID ,b.TITLE ,r.NAME ,trunc(br.BORROW_DATE) AS borrow_day ,trunc(br.DUE_DATE) AS due_day ,CASE br.STATUS WHEN 0 THEN 借出 WHEN 1 THEN 已还 WHEN 2 THEN 逾期 END AS status_text FROM BORROW br JOIN BOOK b ON b.BARCODE br.BARCODE JOIN READER r ON r.READER_ID br.READER_ID WHERE br.BORROW_DATE TRUNC(SYSDATE) - 30 ORDER BY br.BORROW_ID DESC;TRUNC(SYSDATE)返回今天零点减30得到30天前零点这样就能覆盖整个30天窗口不会把“昨天的记录但是今天凌晨的时分秒”漏掉。查询结果的status_text可以直接回填到前端表格上。注意看这里没有说什么“通过该语句能提升性能”之类的空话纯粹是日期条件写法上的一个边界细节性能要靠BORROW_DATE上的索引来解决可函数索引是另一个话题。5. Oracle环境里的高频故障从监听服务到JDBC驱动5.1 安装11g/12c后监听服务无法启动的排查路径图书管理系统一旦换电脑部署第一个卡住的地方往往是Oracle环境。监听服务TNSLSNR起不来是热搜里最高频的问题也是真实环境最烦人的问题。我一般按三步排查第一步看listener.ora里的主机名是否和实际IP一致如果配置的是localhost而别的主机要连接必然起不来或连不上第二步检查端口是否被占用netstat -ano | findstr 1521被防火墙或另一个实例占住时监听会立刻退出第三步看ORACLE_HOME环境变量是不是指向了正确路径安装了多个Oracle客户端时PATH里先出现的那个影响最大。# 查看监听状态 lsnrctl status # 启动监听 lsnrctl start # 若启动失败打开监听日志通常在 $ORACLE_HOME/network/log/listener.log还要注意Windows和Linux差异。Windows上服务名叫OracleOraDB11g_home1TNSListenerLinux上用lsnrctl就能管理。如果lsnrctl start报“No such listener”或“TNS-12560”多半是ORACLE_HOME不对。你可以在命令行执行echo %ORACLE_HOME%或echo $ORACLE_HOME确认。5.2 连接时缺少驱动/ORA-12505/ORA-01017Java项目连Oracle需要ojdbc驱动Maven仓库里的坐标常用这个dependency groupIdcom.oracle.database.jdbc/groupId artifactIdojdbc8/artifactId version21.3.0.0/version /dependency这是Oracle官方在Maven Central发布的驱动坐标比下载jar手动拷进WEB-INF/lib稳定得多。不过要注意版本对应Oracle 11g可以用ojdbc612c以上用ojdbc8瞎用一个旧驱动会报UnsupportedClassVersionError或ORA-03135。连接串里最常见的错误是服务名写错jdbc:oracle:thin://localhost:1521/ORCL这里的ORCL是监听里的服务名不是SID。如果监听配置的是SID应该写jdbc:oracle:thin:localhost:1521:ORCL两种格式冒号斜杠不同写混淆了就会报ORA-12505监听器无法处理给定的连接描述符或ORA-12514。ORA-01017用户名或口令无效则要检查密码是否带了引号和特殊字符。我在第3章建用户时把密码写成Lib_2024是有意为之包含下划线、数字和大小写某些框架属性文件里要注意转义。如果使用GRANT CONNECT, RESOURCE TO LIBRARY后仍然连接失败先sqlplus library/Lib_2024ORCL在服务器本机测试能通过再用JDBC连这样能快速分离数据库层和应用层问题。5.3 中文乱码与字符集AL32UTF8的检查图书管理系统里满是书名、作者名中文乱码会一直阴魂不散地出现在查询和页面上。根因是Oracle客户端字符集和数据库字符集不一致。查看数据库字符集SELECT value FROM nls_database_parameters WHERE parameter NLS_CHARACTERSET;安装时如果选了ZHS16GBK数据库只能存GBK中文客户端用UTF-8连接乱码多半是NLS_LANG环境变量没配对。我习惯统一为AL32UTF8它的好处是能兼容绝大多数应用输入。如果建库时已经是AL32UTF8但页面还乱码那就是JDBC URL缺了characterEncodingUTF-8或者Windows注册表里的HOME环境没配对。另外提醒一句VARCHAR2按字节长度定义一个中文在AL32UTF8下占3个字节所以表定义里VARCHAR2(200)最多放66个汉字乱码和插入失败往往同时出现这时调整的其实是字段长度不是字符集。6. 从数据字典取回文档用SQL生成.doc的设计附录课程设计答辩时老师会翻看.doc里的“数据库表结构”部分。手敲表格容易和实际建库不一致更聪明的做法是从Oracle数据字典取出真实结构再贴回Word。这个技巧能让你的文档永不落后于代码。SELECT uc.table_name AS 表名 ,uc.column_name AS 字段名 ,ut.data_type AS 类型 ,ut.data_length AS 长度 ,uc.nullable AS 可空 ,utc.comments AS 注释 FROM user_tab_columns uc JOIN user_col_comments utc ON uc.table_name utc.table_name AND uc.column_name utc.column_name WHERE uc.table_name IN (BOOK,READER,BORROW) ORDER BY uc.table_name, uc.column_id;这是数据字典视图的组合查询。user_tab_columns管列定义user_col_comments管列注释二者用表名加列名关联。执行结果可以直接在SQL*Plus里SPOOL成C:\output.txt再用Excel分列整理成Word表格。更自动化一点用SELECT DBMS_METADATA.GET_DDL(TABLE,BOOK) FROM DUAL;可以生成完整的建表DDL把它保存进.doc的附录里比截图更专业。想要生成ER图或更规范的文档可以用PowerDesigner反向工程。步骤是新建Physical Data Model选择数据库类型Oracle 11g/12c配置JDBC连接然后“Reverse Engineer”选择表空间PowerDesigner会把表、列、约束、索引全部抽取成图形导出成Word报告。很多企业里的“数据库设计说明书”就是这么来的先有库再有图而不是反过来画一堆无效的PowerPoint框图。最后再说一个生成文档的小技巧在SQL*Plus里把SPOOL和DBMS_METADATA结合起来能一次导出所有核心表的CREATE语句。命令如下SET LONG 20000 SET PAGESIZE 0 SPOOL /tmp/book_ddl.sql SELECT DBMS_METADATA.GET_DDL(TABLE, table_name) FROM user_tables WHERE table_name IN (BOOK,READER,BORROW); SPOOL OFF;SET LONG 20000是防止CLOB内容被截断SET PAGESIZE 0去掉分页横线导出结果就是一份可重新执行的建表脚本。把它整理进.doc文件再配上一张ER图这份“Oracle图书管理系统数据库设计与实现”才算真正闭环设计能追溯到现实表结构表结构能还原成设计文档而你就是那个同时看得懂Oracle数据字典和Word排版的人。本文还有配套的精品资源点击获取
返回列表