多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Oracle课程设计实战:图书管理系统的数据库设计与存储过程

Oracle课程设计实战:图书管理系统的数据库设计与存储过程 简介面向高校数据库课程设计的一份Oracle数据库课程设计报告以图书管理系统的完整构建为主线适合正在完成Oracle课设、需要参考设计思路与报告结构的学生。资源为单个Word文档共1个文件压缩包大小225KB当前已有397人浏览学习。报告按实际课程设计流程组织依次涵盖引言、概要设计、数据库分析、详细设计及测试和课程设计心得五大部分详细给出了系统需求分析、图书E-R图、功能模块划分、数据表与用户表设计、登录权限及安全性设计并编写了存储过程与触发器来增强数据校验和自动处理能力。同时报告结合Windows 7 Visual Studio 2005 Oracle 11g开发环境展示了与VC/C/C#前台交互的界面设计和主要代码覆盖图书信息增删改查、出入库管理等典型功能能够帮助读者快速搭建课设框架、理解Oracle数据库从需求到实现的全过程。1. Oracle 数据库课程设计报告拆解先跑通再优化每年课程设计季都会看到一类资料Oracle 数据库课程设计报告前台用 C#后台用 Oracle 11g功能围绕图书管理系统的增删改查展开。这份报告的价值不在于界面有多好看而在于它把数据库课设最常考的几件事串在了一起——创建表空间和用户、设计五张业务表、编写登录与入库的存储过程、用触发器自动维护库存最后用 C# 把数据库操作接到界面上。适合两类人一是被要求用 Oracle 完成管理信息系统课设的学生二是想快速搭一个 Oracle 演示项目、但不想从零开始写库脚本的开发者。下面按“建库 → 存储过程 → 前端桥接 → 排错 → 复用”的顺序拆尽量让每一步都能照着敲。2. 数据库设计为什么先建表空间、再建用户、然后才建表Oracle 和 MySQL 一个很大的区别在于物理存储管理的层级。MySQL 里建库就是一条CREATE DATABASE但 Oracle 里要先有表空间tablespace再建用户并把默认表空间指过去然后才谈建表。这个顺序对新手来说特别容易反着来上来就写CREATE TABLE结果报“用户不存在”或者“表空间不存在”。课程设计报告里这一部分也是最容易被答辩追问的因为很多同学说不清楚为什么要有表空间这一层。实际上表空间对应的是磁盘上的物理数据文件用户逻辑对象都挂在表空间下面相当于“库”和“库里的账号”是分离的。2.1 为什么课设指定 Oracle 11g 而不是 MySQL从题目要求看设计环境是 Windows 7 旗舰版 32 位 Microsoft Visual Studio 2005 Oracle 11g。这套组合放到今天看确实不算新但 Oracle 11g 的语法、存储过程和触发器机制和现在企业里用的 19c、21c 核心逻辑一致。课程设计特意指定 Oracle而不是让所有人用 MySQL是因为 Oracle 有几个能拿来讲的硬知识点本地管理表空间、用户与 Schema 的绑定、存储过程与触发器的编写规范、事务提交回滚的控制。这些在 MySQL 里也存在但 Oracle 的表达更严格——比如SELECT INTO查不到数据会直接抛NO_DATA_FOUND异常MySQL 默认不会这么干。另一个原因是 C# / VC 与 Oracle 的配合是经典的“前台 后台”组合企业里大量遗留系统就是这么搭的。报告里的代码用的是 C#连接方式走的是System.Data.OracleClient这在 .NET 2.0 时代是主流。后面的章节会提到新版环境怎么迁移这是后话。2.2 表空间、用户与授权建库脚本拆开讲常见的做法是先建目录再建表空间。下面的脚本是课程设计里最典型的一段-- 创建表空间数据文件放在 E:\biaokongjian 目录下 create tablespace tushu datafile E:\biaokongjian\tushu.dbf size 32M autoextend on next 32M maxsize 2048M extent management local; -- 创建用户并指定默认表空间 create user wsn identified by 1234 default tablespace tushu; -- 授予基本权限 grant connect, resource to wsn;datafile后面的路径决定了物理数据文件的位置这个目录必须真实存在否则会直接报错。size 32M是初始大小autoextend on表示空间不够时自动扩展每次扩 32Mmaxsize 2048M是上限。extent management local指定本地管理表空间这是 9i 之后就推荐的方式可以避免字典管理表空间时的递归回收问题。接下来是用户授权。报告里只写了创建用户没写grant但实际用wsn登录后建表会提示权限不足。我一般会在这步把connect和resource两个角色都授出去前者允许登录后者允许建表建索引。有些课设还会要求单独授予某个表的增删改权限那就用grant select, insert, update, delete on 表名 to 用户名来精确控制。2.3 五张核心表的字段设计与外键关系这套系统的表规模不算大一共五张用户表yonghu、图书类别表typ、图书表books、入库表InWarehouseitems、库存表stock。其中books表字段最多是整套系统的主表。字段类型约束说明ISBNvarchar2(20)主键图书编号BookNamevarchar2(40)not null图书名称TIDvarchar2(10)外键 → typ(TID)类别编号RetailPricevarchar2(10)not null零售价Authorvarchar2(20)作者Publishvarchar2(30)出版社StockMinnumbernot null库存下限StockMaxnumbernot null库存上限Descriptionsvarchar2(100)图书描述建表语句里的外键约束值得注意books.TID引用typ.TIDInWarehouseitems.ISBN引用books.ISBNstock.ISBN也引用books.ISBN。外键存在的意义是保证引用完整性比如往books表插数据时如果TID在typ表里不存在Oracle 会拒绝插入这比在代码里查一遍再插要可靠得多。InWarehouseitems表里冗余存了BookName和RetailPrice这在工程上是说得通的——入库单要保留入库那一刻的书名和价格快照否则以后books表改了书名或价格历史入库记录就面目全非了。库存在stock表里单独维护StockNum记录当前库存量。一个小提醒RetailPrice用的是varchar2(10)只能做展示没法做价格区间查询和数值累加真实项目里这个字段应该用number(8,2)。课设场景为了省事拿字符串存数字是常见操作你在答辩时如果能主动说出这个改进点反而是加分项。3. 存储过程与触发器把业务逻辑下沉到数据库层课程设计题目里有一句硬性要求涉及数据的所有操作要求采用存储过程的方式进行。这个要求翻译一下就是——前端界面不允许直接insert/update/delete表必须封装成存储过程前端只调过程不碰表。这样做的好处是数据库层面就能把权限收紧应用层的数据库账号只拥有执行存储过程的权限不给表级写权限安全性会好很多。这一章把报告里的登录存储过程、入库存储过程和库存触发器完整走一遍。3.1 登录存储过程一个 flag 参数区分三种状态登录过程的代码是整份报告里最有“课设味”的一段先看整理后的版本create or replace procedure denglu( flag out number, username in varchar2, upwd in number ) as i varchar2(20); p number; begin flag : 0; -- 第一步按用户名查用户 select t.ename into i from yonghu t where t.ename username; -- 第二步用户存在继续校验口令 if i is not null then flag : 1; select t.eno into p from yonghu t where t.ename username and t.eno upwd; if upwd is not null then flag : 2; -- 登录成功 else flag : 1; -- 口令不正确 end if; else flag : 0; -- 用户不存在 end if; commit; exception when no_data_found then rollback; end;这个过程的思路是flag作为输出参数0 表示用户不存在1 表示口令错误2 表示登录成功。前端拿到flag后跳转到不同界面。我第一次看这段代码时觉得逻辑很绕细读才发现它完全依赖 Oracle 的隐式异常行为——select ... into ... where查不到数据时Oracle 会直接跳入exception块而不执行后续语句。所以“用户不存在”这个分支实际是靠着异常从内部跳出来的外层的if i is not null只有在查到用户名时才会继续往下走。这里有个明显的缺陷密码字段用的是eno也就是用户编号本身而不是独立的密码字段。也就是说登录凭证是“用户名 用户编号”编号如果是 1、2、3 这种自增值密码形同虚设任何人试几个数字就能登进去。课设里这种写法很常见因为能少建一个字段、少写一段增删改查。但从真实系统角度出发至少应该给yonghu表补一个pwd varchar2(20)字段用摘要存储而不是暴露原始编号。五章避坑部分还会再提一次。3.2 入库存储过程与自动更新库存的触发器入库逻辑单独抽成了一个存储过程核心是“已有记录则累加数量没有则插入新行”create or replace procedure rk( isb varchar2, bname varchar2, rp varchar2, sl number ) as i number; begin -- 判断该书是否已存在入库记录 select count(*) into i from inwarehouseitems where isbn isb; if (i 0) then -- 已存在累加入库数量 update inwarehouseitems set shuliang shuliang sl where isbn isb; else -- 不存在插入新记录 insert into inwarehouseitems values (isb, bname, rp, sl); end if; end;注意这个过程并没有写commit。这是合理的事务的提交应该交给调用方控制。如果在过程内部直接commit前端程序一旦在后续步骤失败连回滚的机会都没有。入库记录写完后库存表怎么同步靠的是下面的触发器create or replace trigger charu after insert or update on InWarehouseitems referencing old as old new as new for each row declare n_count number(4); begin if updating or inserting then -- 判断库存表里是否已有该图书 select count(*) into n_count from stock where isbn :new.isbn; if n_count 0 then -- 已存在累加库存 update stock set stocknum stocknum :new.shuliang where isbn :new.isbn; else -- 不存在初始化库存 insert into stock(isbn, stocknum) values (:new.isbn, :new.shuliang); end if; end if; end;触发器属于行级触发器for each row表示对每一行变更都执行一次。:new引用的是变更后的新值:old是变更前的旧值这里是插入/更新场景所以主要用:new。if updating or inserting做了操作类型过滤只处理写入和更新不处理删除。整段逻辑的核心是无论入库表是新增一行还是修改数量都把本次变更的量累加到stock表。链条是 前端调用rk存储过程 → 修改InWarehouseitems→ 触发charu触发器 → 自动更新stock全程不需要前端写第二行代码。这套机制能跑通但有个隐藏前提入库表InWarehouseitems没有任何主键约束完全靠“同一 ISBN 就累加”来维护数据。万一同一本书在同一时间被两个人并发入库count(*)0的判断可能同时通过最后插入两行相同 ISBN 的记录库存也加了两遍。课设演示一般不会触发并发问题但你要知道这个边界。改成“每次入库插一行流水记录”是更稳健的设计第 6 章会再讲。3.3 前端调用存储过程的固定套路存储过程写好后C# 侧调用方式比直接拼 SQL 要繁琐一点但模式是固定的OracleCommand cmd new OracleCommand(denglu, database.GetOpen()); cmd.CommandType CommandType.StoredProcedure; // 注册存储过程的三个参数 cmd.Parameters.Add(flag, OracleType.Number).Direction ParameterDirection.Output; cmd.Parameters.Add(username, OracleType.VarChar).Value user1; cmd.Parameters.Add(upwd, OracleType.Number).Value 1234; cmd.ExecuteNonQuery(); // 执行完成后读取输出参数 int flag Convert.ToInt32(cmd.Parameters[flag].Value);存储过程里的in参数对应普通Value赋值out参数对应Direction ParameterDirection.Output执行完ExecuteNonQuery之后再读取输出参数的值。要注意参数名必须和存储过程形参名完全一致Oracle 的驱动是按名字绑定参数的改了名字会对不上。用System.Data.OracleClient时OracleType.Number、OracleType.VarChar都是这个命名空间下的枚举如果换成 ODP.NET类型名会变成OracleDbType.Number写法略有差异但套路不变。前端提交按钮的事件里调用denglu拿到flag再根据 0、1、2 三个值提示“用户不存在 / 密码错误 / 登录成功”就完成了整个登录链路。4. 前端与 Oracle 的桥接C# 连接串、数据访问与 ListView 显示数据库端的表、存储过程、触发器都准备好之后剩下的工作就是把前端界面接到数据库上。报告里这一部分用了很典型的课设写法全局静态连接类 DataTable 中转 ListView 展示。虽然代码组织上偏过程式但作为 200 行以内能跑通的方案效率其实不低。这一章把连接串、连接类、查询显示三段代码逐个拆开讲清楚每个参数的作用和容易被忽略的细节。4.1 连接串与数据库连接类的最小实现先看配置文件里的连接串?xml version1.0 encodingutf-8 ? configuration appSettings add keyConStr valueData Sourceorcl;User IDwsn;Password1234;UnicodeTrue/ /appSettings /configurationData Sourceorcl里的orcl是 Oracle 的服务名必须与tnsnames.ora文件里配置的监听条目一致。如果你本机 Oracle 的服务名不叫orcl这里就要改成实际名称否则连接时报ORA-12514。User ID和Password对应第 2 章创建的wsn用户UnicodeTrue表示使用 UTF-16 与 Oracle 交互中文数据读写更稳妥。连接类封装得很直接class Database { static OracleConnection con new OracleConnection(); public static OracleConnection GetOpen() { try { if (con.State ConnectionState.Closed) { con.ConnectionString ConfigurationSettings.AppSettings[ConStr].ToString(); con.Open(); } return con; } catch (Exception ee) { return null; // 课设简化写法异常只返回空引用 } } public static void GetClose() { if (con.State ConnectionState.Open) { con.Close(); } } }静态字段con保证整个程序里只维护一个连接实例GetOpen里先判断状态再打开避免重复Open报错。这里有个小坑catch块直接return null上层拿到null之后继续用会触发空引用异常而且异常信息被吞掉了根本不知道连接失败的原因是什么。我一般会在这个catch里把ee.Message记到日志或者弹窗显示哪怕是课设也值得把错误信息打出来排查问题能少不少时间。这段代码用的是ConfigurationSettings.AppSettings[ConStr]这是 .NET 1.x 时代的写法VS2005 里能用但会有过时警告更规范的替代是ConfigurationManager.AppSettings[ConStr]。4.2 用 DataAdapter 把 stock 表查出来再显示到 ListView查询库存表的典型做法是 DataAdapter DataTablepublic DataTable ss() { try { OracleDataAdapter oda new OracleDataAdapter(); string sql select * from stock order by ISBN; OracleCommand cmd new OracleCommand(sql, database.GetOpen()); oda.SelectCommand cmd; oda.Fill(dt); // 查询结果填充到内存表 return dt; } catch (Exception eee) { return null; } finally { database.GetClose(); } } public void se() { listView1.Items.Clear(); DataTable dt ss(); foreach (DataRow dr in dt.Rows) { ListViewItem item new ListViewItem(dr[0].ToString()); item.SubItems.Add(dr[1].ToString()); this.listView1.Items.Add(item); } dt.Clear(); }OracleDataAdapter是数据适配器Fill方法执行SelectCommand并把结果填充进DataTable相当于把数据库查询结果搬到内存里。ss()负责查询se()负责把DataTable的行逐条加到ListView中。dr[0]对应结果集第一列ISBNdr[1]对应第二列StockNum所以这个列表只显示两列。这里有个很容易被忽略的前提ListView必须配置成详细信息视图并且提前定义好列头否则什么都不会显示。具体做法是在窗体初始化时设置listView1.View View.Details然后调用listView1.Columns.Add(图书编号)、listView1.Columns.Add(库存量)。第五部分会把这作为一条避坑记录展开写。整套代码的逻辑很直白查出来填内存表内存表塞界面控件。对于字段不多的小系统这种写法比实体类和 ORM 框架省事得多。4.3 为什么课设代码里到处都是 DataTable 而不是实体类报告里的所有数据交互都是 DataTable 中转没有定义Book、Stock之类的实体类。原因很简单bindingsource和ListView直接吃 DataTable省掉了对象到控件的映射代码。这在课设阶段完全够用但放到真实系统里会有两个代价一是 SQL 字符串和业务逻辑耦合在一起表字段一旦改名所有相关方法都要改二是魔法字符串满天飞dr[0]、dr[1]这种索引访问可读性很差。改进方向有两个。第一SQL 改用参数化查询不要拼字符串防止注入例如查询某本书时用select * from books where isbn :isbn再通过cmd.Parameters.Add(isbn, OracleType.VarChar).Value textBox1.Text传参。第二如果表结构稳定可以给常用表定义轻量实体类用适配器或者手写映射把 DataTable 转成对象列表。这两个改进不会破坏现有的数据库端逻辑纯属于前端代码的健壮性升级。5. 常见问题与避坑五条血泪经验让课设一次通过这一章整理的是这套图书管理系统从建库到运行最常见的问题每条都按“现象 → 原因 → 解决”的结构写。这些都是实际演示环境里反复出现过的不只是语法层面的坑更多是设计层面和拷贝粘贴带来的问题。5.1 建表空间报 ORA-01119目录不存在现象执行create tablespace tushu datafile E:\biaokongjian\tushu.dbf ...时Oracle 报ORA-01119 error in creating database file后面通常还跟着ORA-27040文件创建错误。原因E:\biaokongjian这个目录在操作系统里并不存在Oracle 不会自动帮你创建目录只要路径无效建表空间必定失败。解决先确认目录存在或者改用已有的目录。我一般在建库前先执行host mkdir E:\biaokongjian或者在 Windows 资源管理器里先把目录建好。如果不想在 E 盘建目录也可以把路径改到 Oracle 安装目录下的oradata文件夹里但建议单独建目录避免跟数据库实例文件混在一起。这个坑是最常见的属于顺序问题不是技术问题。5.2 System.Data.OracleClient 在新环境翻车与 32/64 位不匹配现象把课设源码放到新版 Visual Studio比如 2019 或 2022里编译找不到System.Data.OracleClient命名空间就算编译过了运行时报BadImageFormatException提示加载 Oracle 客户端库失败。原因System.Data.OracleClient从 .NET Framework 4 开始就被标记为过时在 .NET Core 和 .NET 5 里直接不存在了。另一层问题是位数不匹配——Oracle 11g 客户端如果是 32 位而程序集编译成 64 位Native 库加载也会失败。解决连接到包管理搜索Oracle.ManagedDataAccess并安装把代码开头的using System.Data.OracleClient;换成using Oracle.ManagedDataAccess.Client;连接串可以原样保留。这个托管驱动不依赖本机 Oracle 客户端位数问题也不会出现。若是不能换包的老项目就在项目属性里把平台目标改成x86保证和 32 位 Oracle 客户端一致。5.3 从 Word 复制 SQL 后 PL/SQL Developer 报 PLS-00103现象把报告里的 SQL 语句原封不动粘贴到 PL/SQL Developer 的 SQL 窗口执行报PLS-00103: Encountered the symbol提示在某个单词位置语法错误。原因这份报告是从 Word 文档里复制出来的SQL 里混入了全角空格和不可见字符。所有 SQL 关键字之间出现了被拆开的现象比如update stock被粘成up date s to c k而 PL/SQL 编译器把up当作未定义的符号自然报错。解决不要直接粘贴打开 SQL 窗口后先点击工具栏的“空白字符显示”图标把所有空格和全角空格显示出来肉眼就能看出哪里被拆开。最稳妥的做法是按照报告里的逻辑自己在编辑器里重新敲一遍表结构和存储过程不要复制。重新敲一遍的同时还能顺便把大小写统一成 Oracle 习惯的大写关键字格式。5.4 登录存储过程“玄学失败”异常被隐式吞掉现象调用denglu存储过程后flag始终是 0明明数据库里已经插入了用户记录前端却提示“用户不存在”或者账号密码正确但是flag返回 1。原因这套登录过程把“用户不存在”和“密码错误”的判断依赖在NO_DATA_FOUND异常上。任何一条select into查不到数据程序都会跳过后面的flag赋值直接跳进exception块。如果你插入用户时eno和用户名不匹配或者eno的精度被 Oracle 做了隐式转换都会走上异常通道。解决把异常依赖改成显式判断。利用count(*)代替select into并给用户表增加独立的密码字段create or replace procedure denglu( flag out number, username in varchar2, upwd in varchar2 ) as v_count number; begin flag : 0; -- 同时校验用户名和密码 select count(*) into v_count from yonghu where ename username and pwd upwd; if v_count 0 then flag : 2; else -- 用户名存在但密码不匹配 select count(*) into v_count from yonghu where ename username; if v_count 0 then flag : 1; end if; end if; end;这段逻辑不依赖异常就能区分三种状态而且pwd用varchar2存储不光能存数字还能存带特殊字符的口令。顺手给用户表建一个唯一索引约束ename防止同名用户重复入库。5.5 ListView 一片空白忘了设置 View 和 Columns现象运行程序登录成功之后进入图书管理界面ListView控件是空的一行数据都不显示调试时ss()方法里的DataTable是有数据的dt.Rows.Count大于 0。原因ListView默认的View属性是LargeIcon这个模式下Items集合里的内容只显示图标不显示文本列。而且如果没有给Columns集合添加任何列头就算切到Details视图每一行也只能显示第一列的内容。解决在窗体加载或者se()方法执行之前先设置好视图模式和列头listView1.View View.Details; listView1.Columns.Add(图书编号, 120); listView1.Columns.Add(库存量, 80);Columns.Add的第二个参数是列宽单位是像素。不设置列宽的话列会挤在一起看起来也是“空白”的。这个坑在课设验收现场出现频率极高因为代码逻辑没问题问题全在控件属性配置上。6. 把课程设计沉淀成可复用模板一个轻量 OracleHelper 与后续改进以上五部分把这份报告从头到尾拆了一遍最后这章我习惯做的事是把散落的重复代码收拢成一个小工具类。报告里几乎每个窗体都要写一遍“连接打开 → 创建 Command → 执行 → 关闭”重复性太高不值得每处都复制一遍。一个最简版的 OracleHelper 大概长这样public static class OracleHelper { public static DataTable Query(string sql) { DataTable dt new DataTable(); using (OracleConnection con new OracleConnection(connStr)) { OracleDataAdapter oda new OracleDataAdapter(sql, con); oda.Fill(dt); } return dt; } public static int Execute(string sql) { using (OracleConnection con new OracleConnection(connStr)) { OracleCommand cmd new OracleCommand(sql, con); con.Open(); return cmd.ExecuteNonQuery(); } } }把连接串放进静态只读字段每个方法内部新建连接、用完即释放避免全局连接的状态污染。Query返回 DataTableExecute返回影响行数所有页面共用这两个方法再也不用重复写GetOpen/GetClose。如果业务需要调用存储过程单独加一个带参数的方法把CommandType设为StoredProcedure即可。再往后走三个改进点我最推荐。第一价格字段从varchar2(10)改成number(8,2)让价格可计算也为以后的报表统计留路。第二入库表改成流水式存储每次入库插一行而不是更新数量这样每种图书的历史入库记录都能查到触发器也改写成只对insert生效不再处理update逻辑更简单。第三登录表补上pwd字段后可以再增加失败次数记录连续失败三次锁定账号——这已经是生产环境最常见的防护策略了。那次帮别人调这个系统时我最初怀疑触发器有问题后来发现是ListView列头没设置界面空白和数据库一点关系都没有。从那以后我每次拿到类似的课设 Demo都会强制走一遍“建库脚本 → 存储过程 → 连接串 → 界面绑定”的检查顺序先看数据库再调界面问题定位会快很多。希望这份拆解能帮你把报告里的逻辑完整跑通也能在答辩时讲清楚每个设计取舍背后的原因少走我当年走过的弯路。本文还有配套的精品资源点击获取
返回列表