ARTICLE DETAIL

资讯详情

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

数据库实验五:存储过程与触发器完整实战解析

数据库实验五:存储过程与触发器完整实战解析 简介面向西北工业大学软件学院数据库课程的实验五资源聚焦E-Commerce数据库概念模型设计任务。资源以电商项目描述为背景要求完成完整ER模式适合正在学习数据库建模、需参考ER图与实验报告格式的本科生使用。压缩包共20个文件大小约282KB涵盖cdm概念模型文件、doc实验说明与报告、txt备注以及14张gif操作过程截图便于按步骤复盘从需求分析到ER图绘制的全过程。已有909人学习下载。通过这份资料可获取实验五的完整ER图成品、配套讲解文档和分步操作录屏既能校验自己的设计思路也能为撰写实验报告提供结构参考。整体内容紧凑、指向明确适合作为课程实验的辅助参考。1. 西北工业大学软件学院数据库实验五.zip先搞懂实验五要在哪个数据库上跑拿到这份压缩包第一反应可能是“五”是个编号里面无非是实验指导书加几个 SQL 脚本。但真正打开做过一遍的都知道数据库实验五的难点不在“把表建出来”而在“表和表之间的约束、存储过程里的事务边界、触发器会不会把数据写乱”——这些恰好是实验五的验收点。这份资源把实验要用的建表脚本、初始化数据、可运行的存储过程和触发器样例都整理好了适合正在补实验报告、准备答辩、或者想拿一套完整可跑的数据库课程设计做参考的同学。我的建议是别急着把脚本一次性全执行。先对照实验要求把“这份资源里哪些是题目给的、哪些是参考答案、哪些需要自己改”分清楚再动手。接下来我按“表结构 → 存储过程 → 触发器 → 踩坑 → 验证”的顺序拆每一步都能直接复现。2. 实验五的验收标准与数据表设计先定四张表再谈触发器2.1 从实验要求反推为什么实验五通常落点在“存储过程触发器”数据库实验前四个通常在做“增删改查、索引、视图”到实验五一般会转到“数据库编程”。如果你手头这份实验五的题目描述里出现了“库存不足自动回滚”“订单号自动生成”“保存操作日志”这类词那基本可以确定本次实验的隐藏考点不是 SQL 语法本身而是数据库的完整性约束、事务控制和自动化机制。常见的实验五验收表有五项能提交建库脚本、能提交测试数据、能演示存储过程、能演示触发器、能写清楚设计说明。很多人挂在后面两项因为存储过程和触发器是在“数据库内部”运行的不像 SELECT 查询那样一眼能看到结果。这也是这个压缩包里参考代码的价值所在——它给了你一个可以对照的标准实现而不是让你从零去猜“什么是事务”“什么是触发器”。新浪的实操建议是你先按题目要求把表建好再跑参考答案里的存储过程观察数据变化最后再自己能写一遍。2.2 数据表设计从压缩包里的 SQL 脚本看表结构实验五一般围绕一个“订单系统”或“图书借阅系统”展开表数量在四到六张之间。我这个资源包里的参考脚本核心是商品表、订单表、订单明细表、库存日志表。建表时特别注意两点外键约束方向和约束命名规范。很多同学在 SQL Server 里建表习惯写 “constraint fk_xxx foreign key ...”但实验报告里如果用的工具是 Navicat 或 DataGrip约束名的可见性没那么直观所以建议建表语句里显式命名不要依赖工具自动生成。我一般会这样建基础表-- 商品表 CREATE TABLE dbo.Product ( ProductId INT IDENTITY(1,1) PRIMARY KEY, ProductName NVARCHAR(50) NOT NULL, Stock INT NOT NULL DEFAULT 0, Price DECIMAL(10,2) NOT NULL, CONSTRAINT CK_Product_Stock CHECK (Stock 0) ); -- 订单表 CREATE TABLE dbo.OrderHeader ( OrderId INT IDENTITY(1,1) PRIMARY KEY, OrderNo NVARCHAR(20) NOT NULL, CustomerName NVARCHAR(50) NOT NULL, OrderDate DATETIME NOT NULL DEFAULT GETDATE(), TotalAmount DECIMAL(10,2) NOT NULL DEFAULT 0 ); -- 订单明细表 CREATE TABLE dbo.OrderDetail ( DetailId INT IDENTITY(1,1) PRIMARY KEY, OrderId INT NOT NULL, ProductId INT NOT NULL, Quantity INT NOT NULL, UnitPrice DECIMAL(10,2) NOT NULL, CONSTRAINT FK_OrderDetail_OrderHeader FOREIGN KEY (OrderId) REFERENCES dbo.OrderHeader(OrderId), CONSTRAINT FK_OrderDetail_Product FOREIGN KEY (ProductId) REFERENCES dbo.Product(ProductId) ); -- 库存变更日志表 CREATE TABLE dbo.StockLog ( LogId INT IDENTITY(1,1) PRIMARY KEY, ProductId INT NOT NULL, ChangeType NVARCHAR(20) NOT NULL, ChangeValue INT NOT NULL, CreateTime DATETIME NOT NULL DEFAULT GETDATE() );上面这段建表脚本里Stock字段加了CHECK (Stock 0)这是一种数据库层面的约束能防止库存被扣成负数。这比在应用层写if stock 0要更可靠因为数据库是最后一道防线。OrderDetail表上建了两个外键分别指向订单主表和商品表这样就不会出现“明细属于一个不存在的订单”这种脏数据。2.3 测试数据与主外键约束直接套用我这几段 INSERT很多同学在用可视化工具手动插数据时没感觉等跑 SQL 脚本才发现明细表有外键指向主表主表数据还没插明细表插不进去商品表有IDENTITY自增列强行指定ProductId会被拒绝。所以测试数据的装载顺序必须和约束方向一致先插商品表再插订单主表最后插订单明细表。-- 1. 商品表数据 INSERT INTO dbo.Product (ProductName, Stock, Price) VALUES (N机械键盘, 10, 299.00), (N无线鼠标, 5, 179.50), (NUSB-C 扩展坞, 0, 129.00); -- 2. 订单主表数据 INSERT INTO dbo.OrderHeader (OrderNo, CustomerName, TotalAmount) VALUES (NSO20240613001, N张三, 299.00), (NSO20240613002, N李四, 179.50); -- 3. 订单明细表数据 INSERT INTO dbo.OrderDetail (OrderId, ProductId, Quantity, UnitPrice) VALUES (1, 1, 1, 299.00), (1, 2, 1, 179.50), (2, 3, 1, 129.00);这里的插入顺序是有讲究的先插“被引用方”商品表、订单主表再插“引用方”订单明细表。如果反着来SQL Server 会直接报外键冲突错误。我在实验辅导时看到有同学为了省事把外键约束先删掉、插完数据再重新加上这种思路不能说不可以但如果你在实验报告里写了“数据库设计了引用完整性约束”演示时却删掉约束再插数据答辩时很难自圆其说。3. 存储过程与事务把实验五的“扣减库存”写成可回滚的代码3.1 一个完整的事务型存储过程锁、事务与错误处理一起写实验五最常见的功能点是“下单扣库存”。如果直接写两条 UPDATE 语句会出现一种情况第一条 UPDATE 成功了第二条 UPDATE 因为某字段超长或约束失败报错导致库存扣了但订单没生成。这就是典型的“数据不一致”。正确的做法是用显式事务把两步操作包起来任何一个环节失败就回滚。CREATE PROCEDURE dbo.usp_CreateOrder OrderNo NVARCHAR(20), CustomerName NVARCHAR(50), ProductId INT, Quantity INT AS BEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; -- 检查库存是否足够 DECLARE Stock INT, Price DECIMAL(10,2); SELECT Stock Stock, Price Price FROM dbo.Product WITH (UPDLOCK) WHERE ProductId ProductId; IF Stock IS NULL BEGIN THROW 50001, N商品不存在, 1; END; IF Stock Quantity BEGIN THROW 50002, N库存不足, 1; END; -- 扣减库存 UPDATE dbo.Product SET Stock Stock - Quantity WHERE ProductId ProductId; -- 插入订单主表和明细表 INSERT INTO dbo.OrderHeader (OrderNo, CustomerName) VALUES (OrderNo, CustomerName); DECLARE OrderId INT SCOPE_IDENTITY(); INSERT INTO dbo.OrderDetail (OrderId, ProductId, Quantity, UnitPrice) VALUES (OrderId, ProductId, Quantity, Price); COMMIT TRANSACTION; END TRY BEGIN CATCH IF TRANCOUNT 0 ROLLBACK TRANSACTION; THROW; END CATCH END; GO这个存储过程的关键点有三个一是WITH (UPDLOCK)锁提示它会在读取库存时加上更新锁防止两个并发会话同时读到相同库存然后都以为库存够用二是SCOPE_IDENTITY()它拿到的是当前会话当前存储过程里最后插入的自增 ID不会读到别的会话插入的数据三是THROW而不是RAISERROR前者不需要提前定义错误号更简洁且会直接跳到CATCH块回滚。注意在实验报告里写“并发”时你需要把UPDLOCK解释清楚这是和普通SELECT的本质区别。3.2 常用参数与调用方式对应到实验五的“验证”环节存储过程不是建完就完事关键是拿一组数据验证它“对”和“错”两种情形。先调用一次成功场景再调用一次“库存不足”场景观察报错和数据变化。如果用可视化工具直接执行下面这段-- 先看当前商品库存机械键盘 10 件 SELECT * FROM dbo.Product; -- 下单 2 件预期成功 EXEC dbo.usp_CreateOrder NSO20240613003, N王五, 1, 2; -- 再次下单 20 件预期抛错 50002 库存不足 EXEC dbo.usp_CreateOrder NSO20240613004, N赵六, 1, 20;第一次调用会正常提交事务商品表机械键盘的库存从 10 变成 8订单头表多一条SO20240613003订单明细表多一条数量为 2 的记录。第二次调用会触发THROW 50002事务回滚不产生任何新的订单数据机械键盘库存停留在 8。这就是“事务回滚”的直观演示——如果你能在实验报告里体现出前后两次数量的差异比单纯贴代码更有说服力。3.3 把存储过程改成实验需要的“带输出参数”版本实验指导书上有时会要求“存储过程带输出参数”比如下单后把新的库存量或订单号返回出来。这不算难度但要注意输出参数和结果集的区别输出参数是标量值结果集是一张临时虚拟表。此压缩包参考代码里就有一个版本是用NewStock INT OUTPUT直接输出剩余库存。CREATE PROCEDURE dbo.usp_ReduceStockWithOutput ProductId INT, Quantity INT, NewStock INT OUTPUT AS BEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; UPDATE dbo.Product SET Stock Stock - Quantity WHERE ProductId ProductId; SELECT NewStock Stock FROM dbo.Product WHERE ProductId ProductId; COMMIT TRANSACTION; END TRY BEGIN CATCH IF TRANCOUNT 0 ROLLBACK TRANSACTION; THROW; END CATCH END; GO -- 调用示例 DECLARE Stock INT; EXEC dbo.usp_ReduceStockWithOutput ProductId 2, Quantity 1, NewStock Stock OUTPUT; SELECT Stock AS NewStock;这段代码里OUTPUT关键字的位置特别容易被写错声明变量时放在参数类型后面调用时放在传入变量后面且必须带OUTPUT关键字。漏写调用端的关键字存储过程会执行但Stock拿不到值。这是很多新手排查半天找不到原因的经典错误。4. 触发器与自动流水号实验五最容易扣分的地方在这里4.1 用触发器维护“订单日志表”为什么不用应用层代码实验五的第二个高频考点是触发器。常见需求是“当订单明细插入时自动往库存日志表写一条记录”或“当订单状态变更时自动记录操作人与时间”。很多同学会问这些逻辑写在应用后端不是更简单吗理论上确实可以但实验五考察的就是“能不能用数据库机制完成”所以必须用触发器不能用 C# 或 Java 代码代替。在订单明细表上建一个AFTER INSERT触发器插入后自动往StockLog写入一行记录商品、变更类型和数量。这样做的意义在于无论未来应用层怎么改只要数据通过 SQL 插入订单明细日志就一定会生成。这是数据库保证一致性的一种典型手段也是实验报告里值得强调的设计点——把业务规则下沉到数据库层。4.2 外键约束与触发器顺序DML 触发器的执行时机DML 触发器分AFTER和INSTEAD OF两种。实验五里最常用的是AFTER INSERT它在数据已经插入成功后才触发如果在触发器内部想修改数据要注意顺序否则可能引发“递归触发器”问题。CREATE TRIGGER dbo.trg_OrderDetail_Insert_Log ON dbo.OrderDetail AFTER INSERT AS BEGIN SET NOCOUNT ON; INSERT INTO dbo.StockLog (ProductId, ChangeType, ChangeValue) SELECT i.ProductId, NOUT, i.Quantity FROM inserted i; END; GO这个触发器读取inserted虚拟表——SQL Server 在每次 DML 操作时自动生成两张虚表inserted存放新插入的数据行。触发器里不查原表而查inserted是因为你需要在数据“进入”表的一瞬间捕获它原表此刻已经包含了新数据但使用inserted更精确、更标准。执行“下单2件机械键盘”的存储过程后StockLog表会同步出现一条(ProductId1, OUT, 2)的记录。如果触发器建错了表或者写错了事件类型这个日志表就是空的这也是验证“触发器有没有真正生效”的最直接证据。4.3 如何用系统视图快速核对“触发器有没有生效”有时你建了触发器但跑完数据没有预期效果。原因是触发器没生效——它被禁用了。所以每次重跑实验之前先查一遍所有相关触发器是不是ENABLED状态SELECT t.name AS TriggerName, OBJECT_NAME(t.parent_id) AS TableName, t.is_disabled FROM sys.triggers t WHERE OBJECT_NAME(t.parent_id) IN (NOrderDetail, NOrderHeader, NProduct);is_disabled返回0表示启用1表示禁用常见的原因是你在修改表结构时某些工具自动帮你禁用了触发器。这个查询还可以看出触发器和表的绑定关系答辩时被问“你有几个触发器”直接拿这个结果页展示即可。5. 实验五避坑指南本地能跑、交上去就出分的5条血泪经验5.1 现象触发器更新另一张表时报“递归触发器”错误你在订单表上建了AFTER UPDATE触发器触发器内部又执行了UPDATE dbo.OrderHeader语句导致同一个表的更新再次触发同一个触发器SQL Server 直接报错并停止操作。原因默认配置下 SQL Server 不允许触发器递归调用自己超过嵌套层数就中断。解决不要在触发器内部更新“本表”只更新其他表如果确实需要更新本表把ALTER DATABASE的RECURSIVE_TRIGGERS打开但这条不推荐——实验报告中很容易被追问成“你的触发器死循环怎么解决”难自圆其说。我一般直接改业务逻辑先算好目标值在触发器外完成本表更新触发器只负责写日志表。5.2 现象存储过程在 Navicat 里执行成功在 SQL Server 里报错你在 Navicat 或 DataGrip 里写好的CREATE PROCEDURE跑得很顺换到 SQL Server Management Studio 里面执行报“CREATE PROCEDURE 必须是批处理中的第一条语句”。原因可视化工具将整个文件按多个批次发送某些工具有自己的语义分隔而 SSMS 中CREATE PROCEDURE前面只要有其他语句比如先跑了建表就必须加GO分隔批次。解决直接在每个CREATE PROCEDURE/CREATE TRIGGER前单独加一行GO不要偷懒。这是迁移环境时的常见原因跟你的存储过程逻辑是否对无关。5.3 现象实验报告里写“事务回滚”实际数据没回滚你把存储过程里的条件故意改为IF Stock Quantity并让它抛错但刷新表发现数据还是变了。检查发现存储过程里根本没有显式的BEGIN TRANSACTION或者你在CATCH块里没有调用ROLLBACK TRANSACTION。SQL Server 默认自动提交事务逐条语句独立生效先前成功的 UPDATE 不会被后续报错影响。解决显式事务 CATCH块里判断TRANCOUNT 0再回滚这是标准写法。实验报告要体现回滚效果最好通过前后数据对比的截图说明不要口头描述。5.4 现象外键约束与装载顺序冲突脚本执行到一半停下你把建表和插入数据的 SQL 放到一个文件里从头执行到中间报“外键冲突”后面的脚本就全停了。原因不是脚本逻辑错是表建立顺序和插入顺序不一致——先建了明细表又先插了明细数据。解决把脚本拆成两段第一段建表第二段插数据插入数据严格按照“主表 → 子表”的顺序。另外注意数据量如果一张表有数百行建议一次性批处理插入避免逐行提交导致性能下降。你可以在实验报告里写清楚“外部键约束的加入时机”很多实验评分表对这个点是有加分的。5.5 现象效果截图与实验要求“界面”对应不上有些实验五题目前面写了用 Java 或 Python 连接数据库做展示界面后面又要求“通过 T-SQL 完成实验”。你在 SQL Server 里跑指令、截图然后再写一个自己写的 Web 页面两者对不上。原因你以为要同时交付两套代码其实实验五的验收一般以“数据库对象和脚本”为主界面只是演示手段。解决先直接执行写好的存储过程和触发器把关键验证结果截图存档再配一个本地控制台应用的调用截图如果时间来得及再做页面。别一开始就花大量时间造前端页面来“装饰”数据库实验。6. 实验五的二次验证把“能跑”升级成“讲得清”的答辩点不少同学的实验五停留在“能跑”层面存储过程能调用、触发器能建但要问“为什么会这样、怎么证明你对”就答不上来了。我自己带过的课程设计里被问倒最多的位置是“你这个存储过程里的锁有什么作用”“触发器和存储过程的区别是什么”。所以我把最后一步放在“二次验证”上——不是重新实现一遍业务逻辑而是用几个简单动作把数据库行为的证据抓出来。第一个动作是手工构造并发场景。开两个查询窗口第一个窗口执行一个带UPDLOCK的存储过程然后在事务内加一条WAITFOR DELAY 00:00:05模拟处理时间第二个窗口也执行同样的存储过程观察第二个查询会阻塞等待。能看到阻塞等待你就拿到了“并发控制生效”的实证。实验报告里贴出状态图解释UPDLOCK和普通 SELECT 读取的差异分量会明显不一样。第二个动作是用 SQL Server Profiler 或者扩展事件记录触发器调用链。打开 Profiler 选SQL:StmtCompleted和SP:StmtCompleted事件再调用一次下单存储过程你能看到存储过程内部逐条语句的执行顺序和耗时。触发器里写的日志插入会不会被记录、会不会出现在调用链的末尾一目了然。写实验报告时“存储过程内部首先执行库存查询、然后执行扣减、最后插入订单明细”这种描述如果用截图证明说服力远超纯文字。第三个动作是备份恢复的“后悔药”这也是我自己的习惯。每次实验做完在交付前生成一份数据库脚本包含全库结构和数据。这样一旦实验报告提交后发现有数据错误可以在几分钟内恢复到最近一次正常状态而不需要重新手动跑一遍建表和跑数流程-- 完整备份到当前机器上的指定目录 BACKUP DATABASE ExperimentDB TO DISK NC:\DBServer\ExperimentDB_2024.bak WITH INIT, STATS 10;这份备份文件加上通过导出数据层应用程序生成的.dacpac能让你在数据库被改乱时一键恢复到可用状态。从那以后我每次交实验报告前都会强制走一遍“备份 导出脚本 核对触发器状态”的流程再确认无误再打包提交血泪教训换来的习惯希望能帮到你。本文还有配套的精品资源点击获取
返回列表