☰
Oracle+C#网上订餐系统开发实战:从建表到避坑全指南
2026/10/12 3:15:45 网站建设 项目流程

简介:这是一份基于Oracle与C#实现的网上订餐系统课程设计资料包,适合需要完成数据库大作业或了解C/S架构开发流程的高校学生。资源共80个文件,压缩包约1.02MB,主要包含C#源码(.cs)、窗体界面资源(.resx/.resources)、程序配置文件(.config)及可执行文件(.exe),同时附带20页Word实验报告和Oracle数据库脚本,覆盖需求分析、界面实现、核心代码、数据库设计与功能演示全流程。已有3386人学习下载。资料包目录结构清晰,源码与文档分离,便于读者对照实验报告理解项目从表结构设计到业务功能编码的实现思路,可直接作为Oracle编程实践和C#桌面应用开发的参考模板。

1. 网上订餐系统为什么选 Oracle+C#:先把这三个前提想清楚,再动手不亏

很多人在网上订餐系统这个题目上卡住,往往不是 C# 界面写不出来,而是栽在 Oracle 的数据访问上。一个很常见的场景:A 同学花了两三天把用户注册、菜品展示、购物车界面全部画完,程序一运行却报 ORA-12541,数据库监听没起来,后面的事全停摆。这个题目的技术关键不在控件拖拽,而在 Oracle 的表设计、存储过程和事务处理,这三块只要有一个没理顺,后面写多少界面都白搭。

网上订餐系统本质上就是三个实体:用户、菜品、订单。C# 负责把这些实体的增删改查做成界面和交互,Oracle 负责数据落地和业务规则约束。选这个组合,最常见的原因是课程指定了 Oracle 数据库,也有不少人是想通过大作业把企业级数据库的常用能力走一遍,序列、触发器、存储过程、事务,这些在答辩和面试里都经常被问到,提前自己动手做一遍,比背概念有用得多。

这篇文章从选型讲起,先帮你确认架构和功能边界,再给完整的建表脚本、C# 连接与业务代码,最后把最容易翻车的几个点逐个排查一遍。无论你是刚拿到题目,还是已经写了一半卡住了,照着这套框架走,能省下不少折腾时间。

2. 系统架构与功能拆分:把复杂度降下来再做,思路就顺了

选型这件事,很多新手一上来就纠结 C/S 还是 B/S,然后陷入技术选型的黑洞,反倒把大作业最核心的业务逻辑丢在一边。我给你的建议是:如果课程没有强制要求 Web 页面,直接选 WinForms 客户端直连 Oracle,这是投入产出比最高的方案。

2.1 C/S 还是 B/S:大作业场景下为什么首选 WinForms

大作业的场景有三个特点:开发周期短、需要现场演示、以功能正确性为第一优先。WinForms 在这三点上都有明显优势。C/S 架构下,你拖一个 DataGridView 绑定数据源,双击控件写事件,F5 就能跑起来,遇到问题可以直接断点调试,定位非常快。而在 B/S 架构里,你要额外处理 IIS 部署、浏览器兼容、异步请求这些和业务无关的问题,对第一次接触 Oracle 的人来说,这些干扰项会让核心业务的学习曲线变得更陡。

从答辩角度讲,C/S 程序演示时直接打开 exe,点几下就能把整个业务流程走完,老师看着直观。B/S 的话还要确保 IIS 服务和端口在答辩机器上都正常,多一道风险。我见过太多人 Web 版页面写了一半,最后因为部署问题被迫重写客户端的情况。如果课程确实指定 Web,那是另一条路线,但这篇文章接下来的内容以最常见的 WinForms 直连 Oracle 为主线。

下表把两种方案的差异列清楚,你按自己的课程要求对号入座:

对比项WinForms 直连 OracleASP.NET Web 连 Oracle
开发效率拖控件,事件驱动,上手快需要处理 HTTP、Session、部署
调试方式断点直接定位到窗体逻辑需要结合浏览器开发者工具
答辩演示启动 exe 即可,环境依赖少依赖 IIS 和端口配置
适用场景绝大多数课设大作业课程强制要求或想走前端路线

选 WinForms 还有一个隐性好处:Oracle 驱动我们只用托管驱动,部署到答辩机器上不需要额外装 Oracle 客户端,这个在第五章会展开讲,能帮你避开一个大坑。

2.2 功能模块清单:用户端与管理端怎么划分

网上订餐系统听起来功能很多,但拆开看就是两个角色、五六个模块。用户端做注册登录、浏览菜品、加入购物车、下单、查看历史订单;管理端做菜品管理、订单管理、用户管理。把这两个角度分开,数据库表结构和代码分层,不会乱。

我一般会先画一张模块清单,把每个模块要做什么、涉及哪几张表列出来,再开始写代码。大致如下:

角色模块核心功能点涉及表
用户账号管理注册、登录、密码加密存储t_user
用户菜品展示分类浏览、分页、搜索t_dish
用户购物车加菜、改数量、删除内存集合
用户下单支付提交订单、生成订单明细、扣减t_order, t_order_item
用户历史订单查看自己的订单列表和详情t_order, t_order_item
管理员菜品管理新增、修改价格、上下架t_dish
管理员订单处理查看所有订单、修改状态t_order
管理员用户管理查看用户列表、禁用用户t_user

功能优先级上,我的建议是先把“用户注册登录”“菜品展示”“下单”这条主链路做通,再去做购物车和管理端。因为这一条链路走通,意味着你把 Oracle 的增删改查、事务、参数化查询全部练了一遍,后面都是重复操作。

2.3 分三层写代码:UI、业务、数据访问分开,别写成一坨

大作业只有一个人写,很多人图省事把 SQL 直接写在按钮的点击事件里,一个窗口几百行。这样不是不能跑,但后面加功能会非常痛苦,尤其是订单逻辑出问题时,你根本不知道去哪一行查。我的习惯是分三层:WinForms 窗体只负责界面展示和事件分发,业务规则放 BLL 层,数据库操作全部收拢到 DAL 层。这样层次清楚,哪个环节出问题就去哪层找。

具体到代码组织,我习惯这样分文件:

OrderSystem.sln ├── OrderSystem.UI // WinForms 窗体、控件事件 │ ├── LoginForm.cs │ ├── MainForm.cs │ └── OrderForm.cs ├── OrderSystem.BLL // 业务逻辑,例如订单金额计算、状态校验 │ ├── UserService.cs │ ├── DishService.cs │ └── OrderService.cs ├── OrderSystem.DAL // 数据访问,只做 SQL 和参数封装 │ ├── OracleHelper.cs │ ├── UserDao.cs │ ├── DishDao.cs │ └── OrderDao.cs └── OrderSystem.Model // 实体类 ├── User.cs ├── Dish.cs ├── Order.cs └── OrderItem.cs

业务逻辑放在 BLL 层,比如下单时要先校验菜品是否存在、计算总金额、再调用 DAL 写入数据库;数据访问层只负责把 SQL 发出去、把结果集塞进实体。这样分工,答辩时老师问你“订单金额哪里算的”,你能直接说出在哪一层、哪一行,得分点就拿到了。

2.4 先封装一个 OracleHelper:后面的代码全靠它

写数据访问之前,我建议先做一个简单的 OracleHelper,把连接管理、通用查询、执行非查询这三块封装好。然后所有 DAO 都走这个入口,统一管理连接字符串和异常处理。这样写代码的时候不会到处都是连接字符串。

NuGet 搜索 Oracle.ManagedDataAccess 并把引用装好之后,项目里直接用这个类:

using System.Data; using Oracle.ManagedDataAccess.Client; namespace OrderSystem.DAL { public static class OracleHelper { private static string connStr = "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1521))" + "(CONNECT_DATA=(SERVICE_NAME=XE)));User Id=order_sys;Password=123456;"; public static OracleConnection GetConnection() { var conn = new OracleConnection(connStr); conn.Open(); return conn; } public static int ExecuteNonQuery(string sql, params OracleParameter[] paras) { using (var conn = GetConnection()) using (var cmd = new OracleCommand(sql, conn)) { if (paras != null) cmd.Parameters.AddRange(paras); return cmd.ExecuteNonQuery(); } } public static DataTable ExecuteDataTable(string sql, params OracleParameter[] paras) { using (var conn = GetConnection()) using (var cmd = new OracleCommand(sql, conn)) { if (paras != null) cmd.Parameters.AddRange(paras); using (var adapter = new OracleDataAdapter(cmd)) { var dt = new DataTable(); adapter.Fill(dt); return dt; } } } } }

这段代码里,连接字符串的 HOST 是数据库所在机器的 IP,本机就写 127.0.0.1;PORT 默认是 1521;SERVICE_NAME 是 Oracle 的服务名。这里有一个新手最容易迷糊的地方:Express 版默认服务名是 XE,完整版可能是 orcl 或者你在安装时自定义的全局数据库名,写错了会报 ORA-12514。先按你本机 Oracle 的实际配置来,后面第 5 章会给排查方法。

GetConnection 里主动 Open 了一下,是让调用方拿到的就是可用连接。这里有个细节:OracleHelper 里的方法都用 using 包裹,保证连接用完后被释放,不会把连接池耗空。很多大作业跑一段时间就报连接池满,多数是因为连接没关,留在那里占着坑。

3. Oracle 数据库设计:四张表和一份存储过程,撑起整个订餐系统

数据库设计是这篇大作业的地基。网上订餐系统的核心就是订单,订单牵扯用户和菜品,所以最少需要四张表:用户表、菜品表、订单表、订单明细表。这四张表的关系很清晰,订单属于用户,订单明细属于订单,订单明细又引用菜品。

3.1 四张核心表:从用户到订单明细,字段这样设计就够用

我给你的这组建表脚本,是大作业最标准的模板,字段数量不多不少,刚好满足功能需求,也不会有设计冗余。直接复制到 SQL Plus 或 PL/SQL Developer 里执行就可以。

CREATE TABLE t_user ( user_id NUMBER PRIMARY KEY, username VARCHAR2(50) NOT NULL UNIQUE, password VARCHAR2(64) NOT NULL, real_name VARCHAR2(50), phone VARCHAR2(20), address VARCHAR2(200), create_time DATE DEFAULT SYSDATE ); CREATE TABLE t_dish ( dish_id NUMBER PRIMARY KEY, dish_name VARCHAR2(100) NOT NULL, price NUMBER(10,2) NOT NULL CHECK (price > 0), category VARCHAR2(50), image_path VARCHAR2(200), is_active NUMBER(1) DEFAULT 1 ); CREATE TABLE t_order ( order_id NUMBER PRIMARY KEY, user_id NUMBER NOT NULL REFERENCES t_user(user_id), total_amount NUMBER(10,2) NOT NULL, status VARCHAR2(20) DEFAULT '已支付', address VARCHAR2(200), create_time DATE DEFAULT SYSDATE ); CREATE TABLE t_order_item ( item_id NUMBER PRIMARY KEY, order_id NUMBER NOT NULL REFERENCES t_order(order_id), dish_id NUMBER NOT NULL REFERENCES t_dish(dish_id), quantity NUMBER NOT NULL, price NUMBER(10,2) NOT NULL, subtotal NUMBER(10,2) NOT NULL );

几个字段设计上的说明:t_user 的 password 用 VARCHAR2(64),是因为我们准备用 SHA256 摘要之后存十六进制字符串,长度固定 64 位,如果你用 MD5 是 32 位,用 VARCHAR2(64) 也可以覆盖。t_order 里存一个 address 字段,是用户下单时地址的快照,这样哪怕用户后面改了收货地址,历史订单依然保留当时的地址信息。t_order_item 里的 price 和 subtotal,同样是快照,菜品价格以后会调整,但订单明细必须保留下单那一刻的单价和总价。

t_dish 里有个 is_active 字段,这是我在大作业里比较推荐的做法,相当于软删除。菜品下架时把它的值改成 0,界面就不显示,但订单明细里的外键引用不会被破坏。这比物理删除菜品更安全,后面讲解题技巧时还会专门提。

3.2 序列加触发器:Oracle 里表达主键自增的“正规姿势”

用过 SQL Server 或 MySQL 的人,刚转到 Oracle 时最不习惯的就是没有自增列。MySQL 里一句 AUTO_INCREMENT 就搞定的事,Oracle 里要用序列加触发器组合实现。这个知识点几乎是 Oracle 面试必问,也是答辩时老师大概率会问的,在这里写清楚。

先看序列:它就是一个独立的递增计数器,不属于某一张表,可以被任何表或语句调用。再看触发器:在往 t_order 表插入数据之前,自动从序列里取下一个值,塞给主键字段。

CREATE SEQUENCE seq_order_id START WITH 1 INCREMENT BY 1 NOCACHE; CREATE OR REPLACE TRIGGER trg_order_id BEFORE INSERT ON t_order FOR EACH ROW BEGIN SELECT seq_order_id.NEXTVAL INTO :NEW.order_id FROM DUAL; END; /

触发器的逻辑很简单:BEFORE INSERT 表示插入动作发生之前触发,:NEW.order_id 是即将写入表的那一行数据的 order_id 字段的引用,我们在这里把序列的下一个值赋给它。对于 t_user、t_dish、t_order_item,你需要创建同样模式的序列和触发器,只要把对象名和表名字段对应改一下。

有一个小细节要注意:序列的 NOCACHE 表示不预取序列值到内存,不会跳号;如果默认带 CACHE,数据库重启后可能出现主键跳号,虽然不影响功能,但答辩时如果被追问,解释起来比较费劲。另外,往表里插入数据的时候,不要再显式写主键值,否则触发器没有机会介入,你手工给的主键可能与序列维护的值冲突,报出那个经典的 ORA-00001 主键冲突错误。

3.3 存储过程:把一次下单做成一个原子的数据库事务

下单这个动作,至少要做两件数据库操作:往 t_order 插一条订单主记录,再往 t_order_item 插若干条明细。如果第一条插成功、第二条明细插入时失败,订单头就成了孤儿数据,用户没收到订单却占了一个订单号。所以这两件事必须包在同一个事务里,要么全成功,要么全失败。

用存储过程处理是最“Oracle”的方式,后端调用一个存储过程,数据库内部保证原子性。既然题目强调基于 Oracle 实现,我强烈建议这一块用存储过程来做,这是整个项目最闪亮的加分点。

这里给出一个完整的存储过程示例,使用了 Oracle 的嵌套表类型来接收多条明细数据:

CREATE OR REPLACE TYPE order_item_t AS OBJECT ( dish_id NUMBER, quantity NUMBER ); / CREATE OR REPLACE TYPE order_item_tt AS TABLE OF order_item_t; / CREATE OR REPLACE PROCEDURE proc_create_order( p_user_id IN t_order.user_id%TYPE, p_address IN t_order.address%TYPE, p_items IN order_item_tt, p_order_id OUT t_order.order_id%TYPE ) IS v_total NUMBER := 0; v_price NUMBER(10,2); BEGIN FOR i IN 1..p_items.COUNT LOOP SELECT price INTO v_price FROM t_dish WHERE dish_id = p_items(i).dish_id; v_total := v_total + v_price * p_items(i).quantity; END LOOP; INSERT INTO t_order(user_id, total_amount, status, address) VALUES (p_user_id, v_total, '已支付', p_address) RETURNING order_id INTO p_order_id; FOR i IN 1..p_items.COUNT LOOP SELECT price INTO v_price FROM t_dish WHERE dish_id = p_items(i).dish_id; INSERT INTO t_order_item(order_id, dish_id, quantity, price, subtotal) VALUES (p_order_id, p_items(i).dish_id, p_items(i).quantity, v_price, v_price * p_items(i).quantity); END LOOP; COMMIT; EXCEPTION WHEN OTHERS THEN ROLLBACK; RAISE; END; /

先说业务逻辑:过程接收用户 ID、收货地址和一个菜品明细集合。第一步遍历明细算总金额,算的过程里顺便从 t_dish 查出当前单价。第二步插入订单主表,通过 RETURNING 把新生成的主键返回出来。第三步循环明细集合,插入明细行。最后提交事务。

事务控制是这段代码的关键:过程体里一旦有异常,会先 ROLLBACK 回滚,保证已经插入的订单头也被撤销,避免脏数据,然后 RAISE 把异常继续抛出给调用方,C# 端就能感知到失败。这个“要么全成功要么全失败”的机制,正是校园大作业里很多人没做、但老师特别在意的一个点。

3.4 菜品分页查询:Oracle 的 ROWNUM 写法与注意事项

网上订餐系统的菜品数量一多,一次性全部加载到界面上会很卡,也显得不专业。Oracle 分页和 SQL Server 的 OFFSET FETCH、MySQL 的 LIMIT 都不一样,它用的是 ROWNUM 这个伪列。很多第一次写 Oracle 的人在这里会困惑,直接把 MySQL 的 LIMIT 搬过来,然后报 ORA-00933。

正确的三层嵌套写法是这样的:

SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT * FROM t_dish ORDER BY dish_id ) t WHERE ROWNUM <= :pageSize * :pageIndex ) WHERE rn > (:pageIndex - 1) * :pageSize;

解释一下这三层嵌套的用途:最内层先按 dish_id 排序,得到一个稳定的顺序;中间层给每一行编上一个从 1 开始的 ROWNUM 序号,同时用 WHERE ROWNUM <= pageSize * pageIndex 把范围上限截住;最外层再用 rn > (pageIndex - 1) * pageSize 把当前页之前的行滤掉。注意 ROWNUM 是在 Oracle 内部生成的行号,不能再直接对 ROWNUM 做大于判断,否则查不出数据,这是新手最容易踩的坑。

如果你用分页控件,把 pageIndex 和 pageSize 作为绑定参数传给 OracleCommand 就行。这个 SQL 放在 DAO 层,前面第 2 章封装的 OracleHelper 刚好可以直接调用。分页逻辑虽然看起来绕,但它确实是 Oracle 的标准答案,答辩时能把这段讲清楚,比你说“我会用框架”更能证明你懂数据库。

4. C# 连接 Oracle 与核心业务实现:从登录到下单的完整代码

数据库表建好了,现在进入 C# 端。连接 Oracle 有两条路线:老式的是引用 Oracle.DataAccess,要求本机安装 Oracle 客户端;新式的是用 Oracle.ManagedDataAccess,纯托管程序集,不需要额外安装客户端。这一章所有代码都基于托管驱动,也是我在大作业里推荐的做法。

4.1 连接三种方式:托管驱动、连接字符串与服务名的坑

先说为什么不用 Oracle.DataAccess。这个老驱动本身依赖本机装 Oracle 客户端,不同版本还有 32 位和 64 位之分。你开发机上装了客户端能跑,拷贝 exe 到答辩机器的纯净环境里,大概率报找不到 Oracle.DataAccess.dll 或者版本不匹配。而 Oracle.ManagedDataAccess 只是一个托管 DLL,项目打包时把它复制过去就能跑,省去一大堆环境问题。

连接字符串的写法,有两种风格。一种是引用了 tnsnames.ora 里的服务名,一种是直接把地址写全。我建议你在代码里直接用完整描述,不依赖 tnsnames.ora 配置,这样换一台电脑部署,只要数据库 IP 和端口对,代码就能连上。

using Oracle.ManagedDataAccess.Client; class ConnTest { static void Main() { string connStr = "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)" + "(HOST=127.0.0.1)(PORT=1521))" + "(CONNECT_DATA=(SERVICE_NAME=XE)));" + "User Id=order_sys;Password=123456;"; using (var conn = new OracleConnection(connStr)) { conn.Open(); Console.WriteLine("数据库连接成功"); } } }

这段代码里,HOST 是数据库所在机器的 IP;PORT 默认 1521;SERVICE_NAME 是数据库服务名,这个值非常容易出错。你可以在数据库机器上执行SELECT value FROM v$parameter WHERE name = 'service_names';查看真实的服务名,也可以直接查看 tnsnames.ora 里的配置。注意 SERVICE_NAME 和 SID 是两个概念,连接串里写的是服务名,不要填 SID。很多人在这里把 orcl 写成 sid 形式导致连接失败,这是个高频问题。

4.2 登录与注册:参数化查询是底线,密码要哈希

用户登录是 C# 端最基本的业务。这里最重要的不是怎么写 SQL,而是怎么防 SQL 注入。大作业里经常见到这样的写法:把文本框里的内容直接拼进 SQL 字符串,这非常危险。我给出的写法是使用 OracleParameter 参数化查询,这是数据访问的底线要求,也是答辩老师可能会追问的点。

先看登录的 DAO 代码:

public User Login(string username, string password) { string sql = "SELECT user_id, username, password, real_name, address " + "FROM t_user WHERE username = :uname AND password = :pwd"; using (var conn = OracleHelper.GetConnection()) using (var cmd = new OracleCommand(sql, conn)) { cmd.Parameters.Add(":uname", OracleDbType.Varchar2).Value = username; cmd.Parameters.Add(":pwd", OracleDbType.Varchar2).Value = password; using (var reader = cmd.ExecuteReader()) { if (reader.Read()) { return new User { UserId = reader.GetInt32(0), Username = reader.GetString(1), RealName = reader.GetString(3), Address = reader.GetString(4) }; } } } return null; }

这里用到的核心点就是:SQL 语句里的:uname、:pwd是绑定参数,实际值由 Oracle 驱动在服务器端绑定,而不是简单拼接字符串。这样即使输入框里输入了' or 1=1 --,它也只是一个普通字符串,成不了攻击 payload。参数名上的冒号是 Oracle 驱动的习惯,写不写冒号都行,但我建议写上,看着清楚。

接下来是注册时的密码处理。我强烈建议不要在数据库里存明文密码,而是用 SHA256 算摘要后存进 t_user 表。用 C# 做哈希只需要几行代码:

using System.Security.Cryptography; using System.Text; public static string HashPassword(string rawPassword) { using (var sha = SHA256.Create()) { byte[] bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(rawPassword)); var sb = new StringBuilder(); foreach (byte b in bytes) { sb.Append(b.ToString("x2")); } return sb.ToString(); } }

注册时把哈希后的结果作为密码写入 t_user,登录时先对用户输入的密码做同样的哈希,再和数据库里存的值比对。这样做的好处是,哪怕你的数据库文件被拷走,别人也无法直接拿到用户的原始密码。这个点在答辩时说出来,项目整体档次一下就上去了。

4.3 下单业务:用 OracleTransaction 包住整个操作

存储过程虽然能包事务,但如果你在 3.3 那一步还没完全消化,C# 端也有一个相对简单可靠的替代方案:用 OracleTransaction 在同一个连接里开启事务,手动控制订单头和明细的插入。这种做法的好处是逻辑都在 C# 里,调试方便,每一步都能用断点观察。

public int CreateOrder(int userId, string address, List<Dish> dishList, List<int> quantityList) { using (var conn = OracleHelper.GetConnection()) { using (var tx = conn.BeginTransaction()) { try { int orderId; string sqlHead = "INSERT INTO t_order(user_id, total_amount, status, address) " + "VALUES (:uid, :total, '已支付', :addr) " + "RETURNING order_id INTO :orderId"; using (var cmdHead = new OracleCommand(sqlHead, conn)) { cmdHead.Transaction = tx; decimal total = 0; for (int i = 0; i < dishList.Count; i++) { total += dishList[i].Price * quantityList[i]; } cmdHead.Parameters.Add(":uid", OracleDbType.Int32).Value = userId; cmdHead.Parameters.Add(":total", OracleDbType.Decimal).Value = total; cmdHead.Parameters.Add(":addr", OracleDbType.Varchar2).Value = address; var pOrderId = new OracleParameter(":orderId", OracleDbType.Int32) { Direction = System.Data.ParameterDirection.Output }; cmdHead.Parameters.Add(pOrderId); cmdHead.ExecuteNonQuery(); orderId = Convert.ToInt32(pOrderId.Value); } string sqlItem = "INSERT INTO t_order_item(order_id, dish_id, quantity, price, subtotal) " + "VALUES (:orderId, :dishId, :qty, :price, :subtotal)"; using (var cmdItem = new OracleCommand(sqlItem, conn)) { cmdItem.Transaction = tx; for (int i = 0; i < dishList.Count; i++) { cmdItem.Parameters.Clear(); cmdItem.Parameters.Add(":orderId", OracleDbType.Int32).Value = orderId; cmdItem.Parameters.Add(":dishId", OracleDbType.Int32).Value = dishList[i].DishId; cmdItem.Parameters.Add(":qty", OracleDbType.Int32).Value = quantityList[i]; cmdItem.Parameters.Add(":price", OracleDbType.Decimal).Value = dishList[i].Price; cmdItem.Parameters.Add(":subtotal", OracleDbType.Decimal) .Value = dishList[i].Price * quantityList[i]; cmdItem.ExecuteNonQuery(); } } tx.Commit(); return orderId; } catch { tx.Rollback(); throw; } } } }

这段代码有三个关键点。第一,cmdHead 的 INSERT 语句里用了 RETURNING order_id INTO :orderId,这是 Oracle 专有的语法,用来在执行插入后直接把新订单号取出来,比再查一次序列优雅得多。第二,cmdHead 和 cmdItem 都显式设置了Transaction = tx,让它们运行在同一个数据库事务里,而不是自动提交。第三,catch 里一旦有异常就 Rollback,保证订单头和明细不会出现只写一半的情况。

如果你已经写好了 3.3 的 proc_create_order,那么在 C# 里调用存储过程的思路也类似,把嵌套表当成参数传进去,驱动会自动把内容组装成数组。存储过程方案比分两步插入更内聚,但代码复杂度高一些,我建议先把上面的事务版本跑通,再考虑升级。

4.4 菜品列表与 DataGridView:绑定数据源,注意只查当前页

菜品展示页是用户看到的第一个界面,多数人用 DataGridView 绑定数据表。常见做法是查询全部菜品然后直接绑定,但数据量一上来就会卡。结合 3.4 的分页 SQL,在 DAL 里写一个带页码的查询方法更专业。

public DataTable GetDishPage(int pageIndex, int pageSize) { string sql = "SELECT * FROM ( " + " SELECT t.*, ROWNUM rn FROM ( " + " SELECT * FROM t_dish ORDER BY dish_id " + " ) t WHERE ROWNUM <= :pageSize * :pageIndex " + ") WHERE rn > (:pageIndex - 1) * :pageSize"; return OracleHelper.ExecuteDataTable(sql, new OracleParameter(":pageSize", OracleDbType.Int32) { Value = pageSize }, new OracleParameter(":pageIndex", OracleDbType.Int32) { Value = pageIndex }); }

这里把 pageSize 和 pageIndex 作为参数传入,避免每次拼接整数字符串,也顺手防住了 SQL 注入。界面上放一个上一页、下一页按钮,每点一次换个 pageIndex 重新查询绑定即可。每次只查当前页的数据,比把全表都拖回来再用控件过滤靠谱得多。

DataGridView 绑定这段比较直白:把 GetDishPage 返回的 DataTable 赋值给 DataSource,再设置一下列头显示的文本,例如把 dish_name 显示为“菜品名称”。注意列名如果有下划线,可以单独配置 HeaderText,不要偷懒直接显示英文字段名。用户看着网感好一点,答辩时也显得用心。

5. 避坑指南:Oracle+C#大作业最容易翻车的 5 个场景

这一章说的都是我自己和周围人做大作业时真实踩过的坑。先给结论:遇到问题不要先怀疑代码逻辑,先查数据库能不能连上、字符集对不对、SQL 方言有没有用错。下面按现象、原因、解决的方式来写,方便你排查时对照。

5.1 程序启动连接报 ORA-12541:监听还没起来

现象是 C# 里一 Open 就抛 ORA-12541: TNS: 监听程序无法启动或无法识别。原因可能是 Oracle 监听服务没启动,或者连接字符串里的端口和服务名和实际不符。我见过有人折腾了两个小时改代码,结果只是监听服务没开。

解决分三步:第一步,在 Windows 服务里找到 Oracle 开头的 TNSListener 服务,右键启动,或者直接在命令行执行lsnrctl start;第二步,在数据库本机用tnsping 127.0.0.1验证监听是否响应;第三步,确认连接字符串的 PORT 是 1521,SERVICE_NAME 和你数据库真实服务名一致。如果本机 tnsping 通了,代码还报错,把服务名核对一遍,基本能定位。

5.2 插入的中文变成问号或乱码:字符集不一致

现象是界面输入中文,写入数据库后变成一串问号,或者读出来乱码。原因是数据库字符集与客户端字符集不一致。Oracle 服务端可能是 AL32UTF8,而客户端环境变量 NLS_LANG 没设对,或者 C# 驱动的字符集配置和数据库对不上。

最简单的排查方法:执行SELECT USERENV('language') FROM DUAL;看数据库当前语言环境,再对比你连接串里的配置。常见的做法是在连接字符串里加上;Unicode=True,让托管驱动按 UTF-8 处理。很多老资料推荐的修改 NLS_LANG 环境变量,在新版的托管驱动下已经不太依赖,优先检查连接串和数据库字符集即可。装数据库时如果不是特殊要求,尽量选 AL32UTF8,能少很多麻烦。

5.3 ORA-00933:SQL 命令未正确结束,多半是方言没换过来

现象是明明感觉 SQL 写对了,执行就报 ORA-00933。原因是把 MySQL 或 SQL Server 的习惯带到了 Oracle。最常见就是 SQL 语句末尾多了一个分号,在 C# 的 OracleCommand 里,一句命令末尾不要加;,加了就会报这个错。另一个高频错误是分页用了LIMIT ? OFFSET ?,Oracle 不认,必须换成 3.4 节的 ROWNUM 写法。

还有一个容易忽略的地方:ORDER、USER、COMMENT这类词在 Oracle 里是保留字,如果表名或字段名直接用了它们,查询也会报错。处理办法是给表和字段加上双引号,或者干脆在表设计阶段避开这些词。我们在 3.1 节用的是 t_user、t_dish、t_order 这种带前缀的表名,就是要规避这一类问题,现在你应该明白这样设计并不是随手起的名字。

5.4 插入数据时报 ORA-00001:主键冲突

现象是第一次插入成功,第二次插入同样的数据就报主键冲突。原因通常是插入语句里显式写了主键值,而序列并没有同步递增,下一次插入了同一个值。另一个常见情况是触发器没有建,或者触发表和序列搞错了对象。

解决方法是检查 t_user、t_dish、t_order、t_order_item 各表是否都有对应的序列和触发器,并且插入语句不要给主键赋值。如果你确实需要手动指定主键,那就要同步把序列的当前值ALTER SEQUENCE ... RESTART,但这在代码里非常不优雅。大作业里最省心的方案就是每个表配一个独立序列和同名表触发器,插入时彻底忘记主键字段。

5.5 换电脑运行报“找不到 Oracle.DataAccess”:驱动依赖问题

现象是开发机正常,把整个项目拷到答辩机器上运行,报找不到 Oracle.DataAccess 或版本不匹配。原因是你用了老式驱动,它依赖本机安装的 Oracle 客户端,而答辩环境没有装。这个坑每年都有不少人掉进去。

解决方法是把项目里的 Oracle.DataAccess 引用去掉,换成 Oracle.ManagedDataAccess。替换之后不需要安装任何客户端,代码逻辑和前几节完全一样,只是 using 的命名空间和引用路径变一下。从 NuGet 安装之后,在发布时把对应的 DLL 复制进输出目录即可。这个替换能一次性解决 90% 的部署类问题,强烈建议一上来就用托管驱动,别走老路。

6. 从“能跑”到“能讲好”:演示预案与三个加分项

代码写到这里,系统已经能跑通完整流程了。但我见过不少人在答辩时栽在演示环节,不是因为功能有问题,而是演示顺序乱、操作不连贯,被老师误以为没完成。我建议你把演示脚本当成一个测试用例来走:启动程序,注册新用户,登录,浏览菜品,把一个菜加入购物车,提交订单,然后切到管理员账号,进入订单管理看到刚才的订单,修改状态,最后回到菜品管理新增一个菜品。整个过程控制在五分钟以内,不要临场乱点。

演示之前,把数据库服务、监听、C# 程序的连接字符串全部检查一遍,最好提前一天在答辩机器上完整跑一遍流程。这是老生常谈,但每年都有人因为答辩机器没装服务、数据库没启动而当场翻车。我自己的习惯是做一个启动自检脚本,先把lsnrctl status跑一遍确认监听正常再打开程序。

加分项有三个方向。第一是 3.4 节的分页查询,让菜品列表不卡顿;第二是软删除字段 is_active,保留历史订单的数据完整性;第三是登录密码做哈希存储,体现安全意识。这三个点都不复杂,但能向老师传递一个信号:你不只是把功能拼出来了,而是在认真思考数据安全和系统设计。如果还有余力,可以把订单状态做成一个独立枚举表维护,而不是散落在业务代码里。

讲存储过程的时候,不要只背语法,可以按这个思路组织:先讲下单需要同时写订单头和明细,再讲这样存在数据一致性问题,最后点出用存储过程加事务解决。老师追问异常分支时,就回答EXCEPTION WHEN OTHERS THEN ROLLBACK RAISE,说明失败会回滚同时把异常抛回调用端。这个逻辑链条清晰,比背十句概念都有说服力。

答辩前一天把数据库里造好的测试数据清掉一部分,只保留三四个菜品、一两条订单,演示页面干净清爽。我吃过一次亏:演示时菜品太多,翻了几页找不到需要的菜,场面略显尴尬。从那以后,每次答辩前我都会把演示数据精简到正好能覆盖演示路径,界面清爽,讲解反而更顺畅。希望帮到你。

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

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

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

立即咨询