简介:一份面向Oracle数据库课程大作业的完整实验资料,覆盖网上订餐系统的需求分析、界面实现、核心代码、数据库设计与功能演示,适合正在完成同类课设或希望掌握C#与Oracle集成开发的学生参考。压缩包共80个文件,大小约1.02MB,主要包含31个C#源代码文件、13个界面资源文件、12个编译资源,另有SQL脚本、配置文件、可执行程序以及20页实验报告(doc),从项目搭建到运行验证均有体现,目录结构清晰,便于按模块查阅;编译生成的exe可直接运行,配合说明文档可快速还原数据库环境。目前已有3386人学习/下载。通过源码可了解C#窗体应用与Oracle数据库的完整交互流程,实验报告则详细说明需求分析、表结构设计和核心代码逻辑,并配有功能演示,适合作为课程设计模板或答辩前的复习材料。
1. 网上订餐系统的Oracle大作业:这门课到底考你什么
被一句“基于Oracle和C#实现”堵在选题门口的人,不止你一个。网上订餐系统是Oracle课程设计里最经典的一类题目,它看起来平平无奇,却恰好把数据库课最想考的三件事全串起来了:表结构设计是否规范、SQL和PL/SQL是否写得熟、事务与并发控制是否真理解。C#负责前台交互,Oracle负责后台数据,两者一接,才算把一门数据库课闭环。这篇文章的目标很直接:从建表到下单事务,再到交作业前最容易翻车的几个点,给你一套能照着做、能讲得清、能扛住老师追问的落地方案。新手按章节走,熟手直接跳去第5章和第6章看边界和坑。
2. 先建库再写码:订餐系统表结构设计与建表脚本
2.1 订餐系统的实体划分:从ER模型到五张核心表
网上订餐系统的业务边界比外卖平台小得多,但“用户下单、商家出餐、订单有状态”这条主链路是躲不开的。我一般会先画ER图,再把实体落成表,而不是上来就写建表语句。核心实体有四个:用户、菜品、订单、订单明细。用户和菜品是基础数据,订单是主表,明细是子表。
用户表存账号、姓名、电话、送餐地址,电话和地址是订餐系统的核心业务字段,不能省。菜品表存菜名、价格、分类、库存量,库存量加上是为了让下单时有东西可扣。订单表存下单用户、下单时间、订单状态、总金额。订单明细表存每个订单买了哪些菜、单价、数量、小计,一张订单对应多条明细,这就是最典型的一对多关系。
如果你的大作业要求支持“购物车”,不要单独建购物车表,用订单明细表加一个状态字段来兼作购物车。很多同学在这里会多建一张cart表,徒增冗余不说,下单时还要做一次“从购物车搬到订单明细”的数据复制,没有任何收益。订单明细表里留一个status字段,0表示还在购物车,1表示已提交,逻辑清晰,答辩也好讲。
2.2 数据类型选型:VARCHAR2与NUMBER的边界
Oracle的数据类型和SQL Server、MySQL的习惯差异很大,这是很多转来做Oracle大作业的人第一个不适应的地方。字符类型优先用VARCHAR2,不要用VARCHAR——在Oracle里VARCHAR目前就是VARCHAR2的别名,但你写成VARCHAR2,阅卷老师一眼就知道你用过Oracle。数字类型用NUMBER,它不像SQL Server的INT和DECIMAL分得那么细,一个NUMBER(10,2)就能管住金额。
金额字段要特别注意,订单总金额和明细小计都用NUMBER(10,2),精度保留两位。库存量用NUMBER(8),够用到天荒地老。日期类型用DATE,不要用TIMESTAMP,订餐系统的精度到秒就足够,DATE在Oracle里本身就带时分秒。主键ID用NUMBER(12),配合序列生成,不用自增列——这是Oracle和MySQL最大的习惯差异,Oracle没有自增列这个概念。
-- 用户表 CREATE TABLE users ( user_id NUMBER(12) PRIMARY KEY, username VARCHAR2(50) NOT NULL UNIQUE, password VARCHAR2(100) NOT NULL, real_name VARCHAR2(50) NOT NULL, phone VARCHAR2(20) NOT NULL, address VARCHAR2(200), created_at DATE DEFAULT SYSDATE ); -- 菜品表 CREATE TABLE dishes ( dish_id NUMBER(12) PRIMARY KEY, dish_name VARCHAR2(100) NOT NULL, category VARCHAR2(50) NOT NULL, price NUMBER(10,2) NOT NULL CHECK (price >= 0), stock NUMBER(8) DEFAULT 0, image_url VARCHAR2(200) ); -- 订单主表 CREATE TABLE orders ( order_id NUMBER(12) PRIMARY KEY, user_id NUMBER(12) NOT NULL REFERENCES users(user_id), order_time DATE DEFAULT SYSDATE, status CHAR(1) DEFAULT '0', total_amt NUMBER(10,2) DEFAULT 0, remark VARCHAR2(200) ); -- 订单明细表 CREATE TABLE order_items ( item_id NUMBER(12) PRIMARY KEY, order_id NUMBER(12) NOT NULL REFERENCES orders(order_id), dish_id NUMBER(12) NOT NULL REFERENCES dishes(dish_id), price NUMBER(10,2) NOT NULL, quantity NUMBER(8) NOT NULL CHECK (quantity > 0), status CHAR(1) DEFAULT '0' );这段脚本里有两个细节值得注意。users表的username加了UNIQUE约束,防止注册重名;order_items表的price字段是下单时的快照价,不是去关联dishes表实时查的,因为菜品价格会变,订单一旦生成就不能跟随菜品调价而变化。这个设计在答辩时会被问到,答“价格快照”四个字,老师就知道你理解订单和菜品之间应该是什么关系。
2.3 主键生成:序列加触发器,建表脚本一次跑通
Oracle没有自增列,常规做法是“序列+触发器”。序列负责生成连续递增的数字,触发器在INSERT之前自动把序列值填进主键字段。这套组合是Oracle课程的必考点,建表脚本里必须带上,否则数据访问层要手动查序列、再拼进INSERT语句,代码又丑又容易错。
-- 用户表序列与触发器 CREATE SEQUENCE seq_users START WITH 1 INCREMENT BY 1 NOCACHE; CREATE OR REPLACE TRIGGER trg_users_id BEFORE INSERT ON users FOR EACH ROW BEGIN SELECT seq_users.NEXTVAL INTO :NEW.user_id FROM DUAL; END; / -- 订单表序列与触发器 CREATE SEQUENCE seq_orders START WITH 1 INCREMENT BY 1 NOCACHE; CREATE OR REPLACE TRIGGER trg_orders_id BEFORE INSERT ON orders FOR EACH ROW BEGIN SELECT seq_orders.NEXTVAL INTO :NEW.order_id FROM DUAL; END; /NOCACHE的意思是序列值不预缓存,避免数据库重启后序列跳号,大作业场景完全够用。触发器里的:NEW.user_id是Oracle触发器特有的写法,表示新插入行该字段的值。按这个模式给dishes和order_items各配一套序列和触发器就行。写完之后用一个简单的INSERT验证:插入一条用户数据,再SELECT出来看主键有没有自动生成。这一步验证很关键,很多同学的触发器写错了没发现,直到C#代码里INSERT报错才回头查。
3. C#接Oracle的接入层:选驱动、配连接、封基类
3.1 驱动选择:为什么现在推荐Oracle.ManagedDataAccess
C#连接Oracle的路子有两条:老一代的System.Data.OracleClient,和现在主流的Oracle.ManagedDataAccess。前者是微软早年提供的ODP.NET版本,依赖Oracle客户端安装,早已被标记为过时,新项目不要再碰。后者是Oracle官方提供的托管驱动,不依赖本机装Oracle客户端,一个DLL走天下,NuGet上直接搜Oracle.ManagedDataAccess.Core就能装。
我建议用.NET Framework的话装Oracle.ManagedDataAccess,用.NET Core或.NET 5+就装Oracle.ManagedDataAccess.Core。两个的命名空间都是Oracle.ManagedDataAccess.Client,API几乎一致。选它的另一个理由是部署省心:交大作业时老师的机器未必装了Oracle客户端,你把DLL一起拷过去就能跑,少一个环境变量层面的不确定性。
using Oracle.ManagedDataAccess.Client; string connStr = "Data Source=//127.0.0.1:1521/ORCL;User Id=scott;Password=tiger;"; using (OracleConnection conn = new OracleConnection(connStr)) { conn.Open(); Console.WriteLine("连接成功,Oracle版本:" + conn.ServerVersion); }这段代码里的Data Source写法用的是EZ Connect格式,//IP:端口/服务名。注意是服务名不是SID,两者在Oracle里不是一回事。如果本机Oracle装的是默认配置,服务名通常是ORCL或ORCLPDB1,取决于装的是12c及以后的CDB/PDB结构还是老式的独立实例。如果这行连不通,可以去tnsnames.ora里查实际服务名,别在服务名上死磕。
3.2 连接字符串与配置:让程序在别的机器上也能跑
大作业交付时,老师的电脑上Oracle环境和你本机大概率不一样。把连接字符串硬编码在代码里是最大忌,老师换台机器跑不起来,第一印象就差了。常见做法是放进App.config的connectionStrings节点里,程序启动时动态读取,老师改配置文件就能连上他自己的库。
<configuration> <connectionStrings> <add name="OracleDbContext" connectionString="Data Source=//127.0.0.1:1521/ORCL;User Id=scott;Password=tiger;" providerName="Oracle.ManagedDataAccess.Client" /> </connectionStrings> </configuration>读取时用ConfigurationManager.ConnectionStrings["OracleDbContext"].ConnectionString,注意要引用System.Configuration.dll。配置文件里还有一个容易踩的坑:Oracle.ManagedDataAccess的默认行为会去读tnsnames.ora,如果找不到会自动尝试EZ Connect。保持上面这种写法就行,别混用TNS别名和EZ Connect。密码里如果含特殊字符,在配置文件里需要做XML转义,比如&要写成&,这是被问得最多的小问题之一。
3.3 数据访问基类:参数化查询的统一入口
数据访问层不要每个方法都写一遍Open/Close/Command,封装一个基类是最值得花的十分钟。基类提供两个核心方法:一个执行查询返回DataTable,一个执行增删改返回受影响行数。所有参数必须走OracleParameter,不要拼SQL字符串,这是防SQL注入的底线,也是答辩老师一定会看的地方。
public class OracleHelper { private static string connStr = ConfigurationManager.ConnectionStrings["OracleDbContext"].ConnectionString; public static DataTable Query(string sql, OracleParameter[] parameters = null) { using (OracleConnection conn = new OracleConnection(connStr)) { using (OracleCommand cmd = new OracleCommand(sql, conn)) { if (parameters != null) cmd.Parameters.AddRange(parameters); using (OracleDataAdapter da = new OracleDataAdapter(cmd)) { DataTable dt = new DataTable(); da.Fill(dt); return dt; } } } } public static int Execute(string sql, OracleParameter[] parameters = null) { using (OracleConnection conn = new OracleConnection(connStr)) { using (OracleCommand cmd = new OracleCommand(sql, conn)) { if (parameters != null) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); } } } }注意Query方法没有显式Open,因为OracleDataAdapter.Fill内部会自动打开连接再关闭。Execute方法必须手动Open。参数数组的写法解决了一个经典问题:当SQL里有多个参数时,用AddRange一次绑定,顺序无关紧要,因为OracleCommand参数是按名称匹配的,不是按位置。使用方代码举例如下:
OracleParameter[] parms = { new OracleParameter(":username", "张三"), new OracleParameter(":password", "123456") }; DataTable dt = OracleHelper.Query("SELECT * FROM users WHERE username = :username AND password = :password", parms);Oracle的参数占位符是冒号开头,不是SQL Server的@符号。如果你从SQL Server迁过来,最容易犯的错就是把:username写成@username,运行时直接报ORA-00936缺表达式,这个错我见过无数回。
4. 核心下单流程:用Oracle事务把订单数据写稳
4.1 一次下单背后:订单表、明细表与库存的联动
网上订餐系统最核心的代码不是查询菜品,而是下单。一次下单牵扯三件事:往orders主表插一条订单、往order_items明细表插多条菜品、扣掉dishes表里对应菜品的库存。这三件事要么全成功,要么全失败。用户提交订单后在菜还没上桌前取消,也要把库存加回来,逻辑一样。
事务的作用就是把这几个操作绑成一个原子单元。C#里用OracleTransaction,从OracleConnection.BeginTransaction()拿事务对象,再把多个OracleCommand的Transaction属性指向它。这里有个细节:OracleCommand必须显式设置Transaction属性,不像SQL Server那样开了事务后自动归属,漏掉这一步会导致前面的操作未提交就被后面的异常影响,回滚也不彻底。
库存扣减的顺序也有讲究。先查库存够不够,再扣。查和扣之间有并发窗口:两个用户同时下单同一道菜,都查到库存剩1,都扣成0,实际超卖了。大作业不一定要求解决到这么深,但老师问到并发时你能说出来“应该用SELECT FOR UPDATE加行锁”,这就是加分项。
4.2 C#事务代码:从BeginTransaction到Commit的完整写法
public bool PlaceOrder(int userId, List<CartItem> cartList, string remark) { string connStr = ConfigurationManager.ConnectionStrings["OracleDbContext"].ConnectionString; using (OracleConnection conn = new OracleConnection(connStr)) { conn.Open(); using (OracleTransaction tran = conn.BeginTransaction()) { try { // 1. 插入订单主表,返回新订单号 string sqlOrder = "INSERT INTO orders(user_id, remark) VALUES(:userId, :remark) RETURNING order_id INTO :orderId"; OracleParameter pOrderId = new OracleParameter(":orderId", OracleDbType.Decimal) { Direction = ParameterDirection.Output }; using (OracleCommand cmdOrder = new OracleCommand(sqlOrder, conn)) { cmdOrder.Transaction = tran; cmdOrder.Parameters.Add(new OracleParameter(":userId", userId)); cmdOrder.Parameters.Add(new OracleParameter(":remark", remark)); cmdOrder.Parameters.Add(pOrderId); cmdOrder.ExecuteNonQuery(); decimal orderId = Convert.ToDecimal(pOrderId.Value); // 2. 遍历购物车,逐条插入明细并扣库存 foreach (var item in cartList) { string sqlDetail = @"INSERT INTO order_items(order_id, dish_id, price, quantity) VALUES(:orderId, :dishId, (SELECT price FROM dishes WHERE dish_id = :dishId2), :qty)"; using (OracleCommand cmdItem = new OracleCommand(sqlDetail, conn)) { cmdItem.Transaction = tran; cmdItem.Parameters.Add(new OracleParameter(":orderId", orderId)); cmdItem.Parameters.Add(new OracleParameter(":dishId", item.DishId)); cmdItem.Parameters.Add(new OracleParameter(":dishId2", item.DishId)); cmdItem.Parameters.Add(new OracleParameter(":qty", item.Quantity)); cmdItem.ExecuteNonQuery(); } string sqlStock = "UPDATE dishes SET stock = stock - :qty WHERE dish_id = :dishId AND stock >= :qty"; using (OracleCommand cmdStock = new OracleCommand(sqlStock, conn)) { cmdStock.Transaction = tran; cmdStock.Parameters.Add(new OracleParameter(":qty", item.Quantity)); cmdStock.Parameters.Add(new OracleParameter(":dishId", item.DishId)); int rows = cmdStock.ExecuteNonQuery(); if (rows == 0) throw new Exception("菜品库存不足,下单失败"); } } } tran.Commit(); return true; } catch { tran.Rollback(); throw; } } } }这段代码有四个点要讲清楚。RETURNING INTO是Oracle特有的语法,把插入后生成的主键直接取回内存,避免再查一次序列值,配合触发器自动生成主键非常好用。库存扣减在UPDATE里加了一个stock >= :qty的隐性校验,如果扣成负数则影响行数为0,直接判定库存不足,这比先SELECT再判断更严谨。事务对象必须赋给每一个Command,漏一个就乱了。整段代码里没有用DataAdapter,因为DataAdapter内部会自动提交,在事务里混用DataAdapter会导致事务边界失真。
4.3 存储过程兜底:把事务逻辑放进Oracle更稳妥
C#代码里写事务,优点是逻辑集中在一个方法里、调试直观,缺点是网络开销大:每执行一条SQL就往返一次数据库。如果订单有10条明细,那就是1次INSERT主表加10次INSERT明细加10次UPDATE库存,合计21次网络往返。课程设计的数据量看不出问题,但老师如果追问“性能怎么优化”,存储过程方案能直接堵住这个问题。
CREATE OR REPLACE PROCEDURE proc_place_order ( p_user_id IN NUMBER, p_remark IN VARCHAR2, p_order_id OUT NUMBER ) AS BEGIN INSERT INTO orders(user_id, remark) VALUES (p_user_id, p_remark) RETURNING order_id INTO p_order_id; -- 明细和库存更新在同一事务内,由调用方决定提交或回滚 -- 实际应用中应在C#端调用该过程并整体提交 COMMIT; EXCEPTION WHEN OTHERS THEN ROLLBACK; RAISE; END proc_place_order; /存储过程把多条SQL打包成一个数据库往返,事务边界放在了数据库内部,C#端只要调用一次过程再提交即可。行锁问题存在,但可以加FOR UPDATE锁菜品行来规避。C#调用存储过程的方式和普通SQL命令不同,要注意把CommandType设为StoredProcedure。
using (OracleCommand cmd = new OracleCommand("proc_place_order", conn)) { cmd.CommandType = CommandType.StoredProcedure; cmd.Transaction = tran; cmd.Parameters.Add(new OracleParameter("p_user_id", userId)); cmd.Parameters.Add(new OracleParameter("p_remark", remark)); cmd.Parameters.Add(new OracleParameter("p_order_id", OracleDbType.Decimal) { Direction = ParameterDirection.Output }); cmd.ExecuteNonQuery(); decimal orderId = Convert.ToDecimal(cmd.Parameters["p_order_id"].Value); }C#调存储过程时,参数名不带冒号也不会报错,但建议保持一致。最常见的报错是ORA-06550,一般是过程里SQL写错或者变量名和列名冲突,把过程体单独拿到PL/SQL Developer里调试一遍,再回C#调用,排查效率高得多。
5. 常见问题排查:Oracle驱动、字符集与事务的五大翻车点
5.1 ORA-12541:连接不上时的第一反应
现象:C#程序一跑就抛ORA-12541,提示TNS监听器不可用。C#里代码看着没问题,SQLPlus能连上,程序连不上。原因有两个方向:一是Oracle监听服务没启动,二是连接串的端口或服务名不匹配。SQLPlus之所以能连,是因为它用了本机的tnsnames.ora;你的程序用EZ Connect格式直接写死了IP和端口,监听没起或者端口不对就立刻炸。
解决:先检查Oracle监听服务,Windows下用lsnrctl status命令查看监听状态,Linux下用lsnrctl start启动。然后确认连接串里端口1521和你实际配置一致,最后用tnsping ORCL验证服务名是否可解析。如果监听正常,把连接串改成Data Source=//localhost:1521/ORCL再试一次。这一步解决掉大部分ORA-12541。
5.2 中文乱码:字符集不匹配的判定与修正
现象:C#界面显示用户地址和菜品名称全是问号,或者从Oracle查询出来的中文是乱码,但SQL*Plus里查询正常。原因是客户端字符集和数据库字符集不一致。Oracle的客户端和服务器通信时,按NLS_LANG环境变量的字符集来转码,C#程序进程里没设这个变量时,Oracle.ManagedDataAccess会有个默认值,很可能和库里的AL32UTF8不匹配。
解决:先查数据库字符集,执行SELECT userenv('language') FROM DUAL拿到类似SIMPLIFIED CHINESE_CHINA.AL32UTF8的值。然后在程序配置文件里加上<add key="NLS_LANG" value="SIMPLIFIED CHINESE_CHINA.AL32UTF8" />,或者更简单,在连接串加;Unicode=True。这一步对.NET的Oracle.ManagedDataAccess尤其重要,很多乱码问题根本不是程序代码写错,就是环境变量层没对齐。改完字符集一定要重新开启连接,连接池里旧的连接不会自动刷新。
5.3 驱动不识别:System.Data.OracleClient退役后的迁移坑
现象:代码编译不报错,运行时抛异常说找不到System.Data.OracleClient,或者提示OracleClient要求安装Oracle客户端。原因是项目里还引着已经被标记过时的微软驱动。现在的.NET Framework机器上,System.Data.OracleClient默认不在程序集列表里,引了旧DLL又没有对应的本机Oracle客户端时,就是这种表现。
解决:把项目里的Oracle引用清理干净,统一换成Oracle.ManagedDataAccess.Client。注意清理csproj文件里的旧引用,还有bin目录下的残留DLL,有次我在项目里引用已换成新的,但bin目录下还有旧的System.Data.OracleClient.dll,运行时就一直往旧驱动上撞,删掉bin再重新生成就好。代码里的using语句也要全部替换,命名空间是Oracle.ManagedDataAccess.Client。
5.4 ORA-00020:连接未释放把数据库进程数打满
现象:程序跑一阵后突然所有查询都报ORA-00020,数据库无法再建立新连接,SQL*Plus登录都进不去。原因是Oracle的processes参数默认值只有150左右,你的C#程序每次操作都new一个OracleConnection,没有及时Dispose的话,连接就占着进程不归还。
解决:严格使用using块保证连接自动释放,数据库端把processes参数调大作为后手。但根本问题是代码里有没有隐藏的连接泄漏,常见泄漏点有两个:一个是非using方式创建的连接只在finally里Close,但Close之前抛异常就没执行到;另一个是DataAdapter.Fill自动打开连接后,程序自己用完了忘记Close。逐个排查后,把进程数参数一并做备份调整,ALTER SYSTEM SET processes=300 SCOPE=SPFILE;然后重启数据库生效。这个参数改动需要权限,大作业里如果只是验证功能,调大恢复即可。
5.5 事务回滚失效:隐含提交与DDL语句的埋伏
现象:事务代码里明明写了Rollback,数据却还在。原因是中间执行了DDL语句,Oracle有隐式提交特性:执行CREATE、DROP、ALTER、TRUNCATE等DDL时,当前事务被自动提交。很多同学在下单事务里试图用CREATE TEMPORARY TABLE来暂存数据,事务从此失效。
解决:事务内部只写DML语句,SELECT/INSERT/UPDATE/DELETE都没问题,绝对不要混入DDL。需要临时数据时用普通表加状态字段,或者用PL/SQL的集合变量在存储过程内部存储中间结果,不要用临时表。这是Oracle事务机制和MySQL/SQL Server最不一样的地方,答辩时说到这个,老师会认为你真踩过坑,印象分会不一样。
6. 交大作业前的加分项:分页、批量插入与演示数据
6.1 分页查询:ROWNUM三层嵌套与结果缓存
菜品列表如果一次性全查出来,界面刷得慢不说,SQL写得也没技术含量。Oracle分页的经典写法是ROWNUM三层嵌套,12c以上可以用OFFSET FETCH,但为了兼容老师那边的版本,用ROWNUM更稳。
SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT dish_id, dish_name, price FROM dishes ORDER BY dish_id ) t WHERE ROWNUM <= :pageSize * :pageIndex ) WHERE rn > (:pageIndex - 1) * :pageSize内层排好序,中层截到当前页末尾,外层滤掉前面页的数据。参数:pageIndex从1开始,:pageSize每页条数。这个写法比OFFSET FETCH的隐患少,因为ROWNUM是在排序后生成的,不会因为ORDER BY字段变化而错乱。分页参数放在SQL里而不是拼在字符串中,不然有注入风险。
6.2 INSERT ALL批量插入:减少往返的一个实用技巧
初始化菜单数据或者生成订单明细时,一条条INSERT既慢又难看。Oracle支持INSERT ALL语句,一次把多行插进同一张表,大大减少往返次数。C#里构造这种语句时要注意DTO数组绑定和最大参数数的限制,一次性别超过几百行,分批次更稳。
INSERT ALL INTO dishes(dish_id, dish_name, category, price, stock) VALUES(seq_dishes.NEXTVAL, '番茄炒蛋', '热菜', 18.00, 100) INTO dishes(dish_id, dish_name, category, price, stock) VALUES(seq_dishes.NEXTVAL, '宫保鸡丁', '热菜', 28.00, 80) INTO dishes(dish_id, dish_name, category, price, stock) VALUES(seq_dishes.NEXTVAL, '米饭', '主食', 2.00, 500) SELECT 1 FROM DUALINSERT ALL后面至少带两个INTO子句。我没说它只能插一张表,但实际上最常用场景就是单表多行插入。配合序列使用时注意,每条INTO子句里都显式调一次NEXTVAL,序列值才是递增的,别写成先取一次序列再复制给三行——那样三行主键就冲突了。演示数据批量造完后,顺便写一句SELECT COUNT(*) FROM dishes验证行数,这是交作业前最廉价的验收手段。
我第一次交这类大作业时,卡在Oracle连不上整整一个晚上,最后发现是监听服务没启动;后来跑通了又发现乱码,改了NLS_LANG才正常。回想起来这些坑都不深,但每踩一个都让人怀疑人生。把上面这套方案从头走一遍,从建库到下单事务,再到环境踩坑排查,你交出去的不只是一份大作业,而是一个能跑、能讲、能应对追问的完整系统。希望帮到你。
本文还有配套的精品资源,点击获取