☰
Oracle课程设计实战:图书管理系统的数据库设计与存储过程
2026/10/9 9:32:22 网站建设 项目流程

简介:面向高校数据库课程设计的一份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表示空间不够时自动扩展,每次扩 32M,maxsize 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.TID,InWarehouseitems.ISBN引用books.ISBN,stock.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 version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="ConStr" value="Data Source=orcl;User ID=wsn;Password=1234;Unicode=True"/> </appSettings> </configuration>

Data Source=orcl里的orcl是 Oracle 的服务名,必须与tnsnames.ora文件里配置的监听条目一致。如果你本机 Oracle 的服务名不叫orcl,这里就要改成实际名称,否则连接时报ORA-12514。User ID和Password对应第 2 章创建的wsn用户,Unicode=True表示使用 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 + DataTable:

public 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]对应结果集第一列(ISBN),dr[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返回 DataTable,Execute返回影响行数,所有页面共用这两个方法,再也不用重复写GetOpen/GetClose。如果业务需要调用存储过程,单独加一个带参数的方法,把CommandType设为StoredProcedure即可。

再往后走,三个改进点我最推荐。第一,价格字段从varchar2(10)改成number(8,2),让价格可计算,也为以后的报表统计留路。第二,入库表改成流水式存储,每次入库插一行而不是更新数量,这样每种图书的历史入库记录都能查到,触发器也改写成只对insert生效,不再处理update,逻辑更简单。第三,登录表补上pwd字段后,可以再增加失败次数记录,连续失败三次锁定账号——这已经是生产环境最常见的防护策略了。

那次帮别人调这个系统时,我最初怀疑触发器有问题,后来发现是ListView列头没设置,界面空白和数据库一点关系都没有。从那以后,我每次拿到类似的课设 Demo,都会强制走一遍“建库脚本 → 存储过程 → 连接串 → 界面绑定”的检查顺序,先看数据库再调界面,问题定位会快很多。希望这份拆解能帮你把报告里的逻辑完整跑通,也能在答辩时讲清楚每个设计取舍背后的原因,少走我当年走过的弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询