☰
数据库实验五实战指南:事务、存储过程与触发器SQL脚本解析
2026/10/31 2:28:11 网站建设 项目流程

简介:这份资源是西北工业大学软件学院数据库实验五的完整提交材料,面向正在学习数据库课程、需要完成E-Commerce项目ER建模的学生。内容围绕电商项目描述中的需求,给出完整的ER模式设计,并附有ER图与文字说明,便于对照理解实体、联系与属性的建模思路。压缩包共20个文件,约282KB,以14个gif操作截图、2个txt与2个doc实验文档、1个cdm概念数据模型文件及1个htm说明页为主,截图覆盖注册、购物车、结算、订单确认等业务流程,文档则记录实验要求与结果。已有911人学习下载,适合需要参考建模过程、核对ER图细节或补做实验报告的同学使用,可从中获取从需求分析到概念模型落地的完整思路与图示素材。

1. 数据库实验五拆包:一份能直接跑通的 SQL 落地资源

数据库这门课,实验一到实验四大多在讲建表、增删改查、视图和约束,到了实验五,画风通常就变了——开始碰事务、存储过程、触发器、游标这些“真家伙”。很多同学前四个实验还能照着课本敲,到实验五就卡在“代码能跑但结果不对”“事务回滚没生效”“触发器递归爆栈”这类问题上。这份西北工业大学软件学院数据库实验五的资源包,价值不在于它有多高深,而在于它把实验五涉及的 SQL 脚本、建库脚本、测试数据和实验报告模板按实验流程组织好了,拿到手就能对着跑一遍,把事务隔离级别、存储过程参数传递、触发器触发时机这些容易翻车的点逐个验证。适合正在做数据库课程实验、需要一份可复现参考脚本的在校生,也适合已经工作但想回头把 SQL 服务端编程补扎实的开发者。下面按“资源里有什么 → 怎么跑起来 → 参数怎么调 → 坑在哪”的顺序拆。

2. 实验五的技术底座:事务、存储过程与触发器怎么串起来

实验五之所以比前面几个实验难,是因为它不再是单条 SQL 的增删改查,而是要求你把多条语句组织成一个逻辑单元,并且保证这个单元要么全成功、要么全失败。资源包里的脚本正是围绕这个核心展开的,所以动手之前得先把三块底座理清楚:事务控制、存储过程、触发器。这三者不是孤立的,实验里常见的组合是“存储过程内部开事务,事务里更新多张表,更新动作又触发触发器做审计”。不理解这个调用链,跑脚本时看到的结果就会和预期对不上。

2.1 事务控制:BEGIN / COMMIT / ROLLBACK 的边界在哪

事务的本质是把一组 DML 语句打包,数据库保证这组语句的原子性。资源里的脚本一般会用BEGIN TRANSACTION或START TRANSACTION开头,中间夹若干INSERT、UPDATE、DELETE,最后用COMMIT提交或ROLLBACK回滚。看起来简单,但边界判断是第一个坎:哪些语句必须在事务内、哪些可以放在事务外,直接决定回滚时数据会不会“回不干净”。

我一般会这样组织一个可验证的事务脚本:

-- 开启事务,注意不同数据库方言写法不同 BEGIN TRANSACTION; -- 第一步:扣减库存,条件里带库存判断防止超卖 UPDATE product SET stock = stock - 1 WHERE product_id = 1001 AND stock >= 1; -- 第二步:写入订单明细 INSERT INTO order_detail (order_id, product_id, quantity) VALUES (2001, 1001, 1); -- 第三步:检查第一步是否真的更新到了行,没更新到就回滚 -- 这里用 @@ROWCOUNT(SQL Server)或 ROW_COUNT()(MySQL)判断 IF @@ROWCOUNT = 0 BEGIN ROLLBACK TRANSACTION; PRINT '库存不足,事务已回滚'; END ELSE BEGIN COMMIT TRANSACTION; PRINT '下单成功,事务已提交'; END

这段脚本的逻辑说明:事务从BEGIN TRANSACTION开始,到COMMIT或ROLLBACK结束,中间任何一步失败都应该走回滚分支。参数上要注意两点,一是WHERE条件里的stock >= 1是防超卖的关键,少了它,库存扣成负数事务照样提交;二是行数判断函数因数据库而异,SQL Server 用@@ROWCOUNT,MySQL 用ROW_COUNT(),PostgreSQL 则要看GET DIAGNOSTICS,资源包如果按某个方言写,换库跑就得改这一行。很多同学跑出来“明明库存不够却下单成功”,八成就是漏了这个判断。

2.2 存储过程:参数模式 IN / OUT / INOUT 的差别

存储过程是实验五的重头戏,资源里的脚本通常会把一段业务逻辑封装成过程,比如“根据商品 ID 和数量生成订单”。这里最容易混淆的是参数模式。IN是只进不出,OUT是只出不进,INOUT是既进又出。实验里常见需求是“传入商品 ID,返回是否下单成功”,这时候就得用OUT参数把结果带出来,而不是靠SELECT打印。

一个典型的带OUT参数的存储过程写法:

-- 创建存储过程,p_result 为输出参数,返回 1 成功 0 失败 CREATE PROCEDURE place_order ( IN p_product_id INT, IN p_quantity INT, OUT p_result INT ) BEGIN -- 声明异常处理器,出错时回滚并置结果为 0 DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SET p_result = 0; END; START TRANSACTION; UPDATE product SET stock = stock - p_quantity WHERE product_id = p_product_id AND stock >= p_quantity; -- 判断是否更新成功 IF ROW_COUNT() = 0 THEN ROLLBACK; SET p_result = 0; ELSE INSERT INTO order_detail (order_id, product_id, quantity) VALUES (FLOOR(RAND() * 100000), p_product_id, p_quantity); COMMIT; SET p_result = 1; END IF; END;

逻辑上,这个过程把“扣库存 + 写订单”包在一个事务里,用异常处理器兜底,任何 SQL 异常都会触发回滚并把p_result置 0。参数说明:p_product_id和p_quantity是输入,调用时必须传实参;p_result是输出,调用前要先声明一个变量接住它。调用方式因方言不同,MySQL 是CALL place_order(1001, 2, @r); SELECT @r;,SQL Server 则是EXEC place_order 1001, 2, @r OUTPUT; SELECT @r;。这里有个血泪经验:OUT参数在过程内部必须先赋值再返回,如果某条分支忘了SET p_result,调用方拿到的就是 NULL,排查起来很费劲。

2.3 触发器:BEFORE 和 AFTER 的触发时机决定能不能改数据

触发器是实验五里最容易被低估的部分。资源包里的触发器脚本一般用于审计,比如“订单表插入后自动往日志表写一条记录”。关键点是触发时机:BEFORE触发器在 DML 执行前触发,可以修改即将插入或更新的数据;AFTER触发器在执行后触发,只能读不能改。实验里如果要求“插入订单时自动补全创建时间”,那就得用BEFORE INSERT;如果要求“插入后记录日志”,用AFTER INSERT。

-- AFTER INSERT 触发器:订单插入后写审计日志 CREATE TRIGGER trg_order_audit AFTER INSERT ON order_detail FOR EACH ROW BEGIN INSERT INTO audit_log (table_name, action_type, record_id, log_time) VALUES ('order_detail', 'INSERT', NEW.order_id, NOW()); END;

这段代码里NEW是关键字,代表刚插入的那行数据,NEW.order_id取的就是新订单的 ID。参数上要注意FOR EACH ROW表示行级触发,每插一行触发一次;如果写成语句级触发,批量插入时就只触发一次,日志会漏记。另一个坑是触发器里不要再对同一张表做写操作,否则容易触发递归或死锁,这个后面避坑章节会细说。

3. 把脚本跑起来:建库、导数据、逐实验验证的完整流程

拿到资源包后不要急着一条条复制粘贴,正确姿势是先按依赖顺序把环境搭好,再按实验步骤逐个验证。资源包里的文件通常分三类:建库建表脚本、实验主体脚本(事务/存储过程/触发器)、测试数据脚本。顺序错了就会出现“表不存在”“外键约束失败”这类低级报错,白白浪费时间。

3.1 环境准备与建库脚本执行

先确认数据库版本。资源包如果没写明方言,从脚本里的@@ROWCOUNT、GO、TOP这些关键字能判断出是 SQL Server 系;如果是ROW_COUNT()、DELIMITER、AUTO_INCREMENT,那就是 MySQL 系。确认之后建一个独立库,别在默认库上跑,避免污染。

# MySQL 环境下建库并导入建表脚本 mysql -u root -p -e "CREATE DATABASE db_lab5 DEFAULT CHARACTER SET utf8mb4;" mysql -u root -p db_lab5 < schema.sql # 导入测试数据 mysql -u root -p db_lab5 < seed_data.sql # 验证表是否建好 mysql -u root -p db_lab5 -e "SHOW TABLES;"

命令说明:-e后面跟的是要执行的 SQL,<是重定向导入文件。utf8mb4字符集建议显式指定,实验里如果有中文商品名或备注,用默认字符集可能存进去变问号。导入完成后用SHOW TABLES确认表数量与脚本预期一致,少表通常是脚本中途报错被截断,回头单独跑报错的那段看提示。SQL Server 环境下对应的是sqlcmd -S localhost -U sa -P 密码 -i schema.sql,逻辑一样,只是命令行工具不同。

3.2 事务实验的验证方法:用两个会话观察隔离级别

事务实验光看脚本跑通不算完,得验证隔离级别是否真的生效。资源里如果涉及“脏读”“不可重复读”的演示,标准做法是开两个会话,一个会话开事务改数据但不提交,另一个会话去读,看读到的是旧值还是新值。

-- 会话 A:开启事务并更新,暂不提交 BEGIN TRANSACTION; UPDATE account SET balance = balance - 100 WHERE account_id = 1; -- 此处先不 COMMIT,保持事务打开 -- 会话 B:在另一个连接里查询同一行 SELECT balance FROM account WHERE account_id = 1; -- 如果读到的是扣减后的值,说明当前隔离级别允许脏读 -- 如果读到的是扣减前的值,说明隔离级别至少是 READ COMMITTED

验证逻辑:会话 B 读到的结果取决于数据库当前隔离级别。MySQL 默认REPEATABLE READ,会话 B 读不到未提交的修改;SQL Server 默认READ COMMITTED,同样读不到脏数据,但会话 B 的读操作会被阻塞直到会话 A 提交或回滚。想演示脏读得手动把隔离级别降到READ UNCOMMITTED,用SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;。这里提醒一句,实验报告里写“观察到了脏读”之前,先确认隔离级别真的改了,否则结论是错的。

3.3 存储过程与触发器的调用验证

存储过程建好后,用CALL或EXEC调用,重点看OUT参数有没有正确返回、异常分支有没有走到。触发器则要验证“该触发的时候触发、不该触发的时候不触发”。

-- 调用存储过程并查看输出参数 CALL place_order(1001, 2, @result); SELECT @result AS order_result; -- 验证触发器:插入一条订单,然后查审计日志 INSERT INTO order_detail (order_id, product_id, quantity) VALUES (3001, 1001, 1); SELECT * FROM audit_log WHERE record_id = 3001; -- 预期能查到一条 INSERT 类型的日志,查不到说明触发器没生效

验证要点:存储过程调用后@result应该是 1 或 0,如果是 NULL,说明某条分支漏了赋值;触发器验证时,audit_log里查不到记录,先确认触发器是否真的创建成功(SHOW TRIGGERS;),再确认插入是否真的执行了。资源包里如果有多个触发器,注意它们之间的执行顺序,同一时机多个触发器的触发顺序在不同数据库里行为不一致,实验报告里最好注明测试环境。

4. 参数与边界:让实验结果对得上的几个关键设置

脚本能跑不等于结果对。实验五的很多“玄学”问题,根源在参数设置和边界条件没对齐。这一章把资源包里最常需要调整的几个点拎出来,包括隔离级别、锁等待、触发器递归开关和存储过程的异常处理,调对了这些,结果才可复现。

4.1 隔离级别与锁等待超时的配置

隔离级别决定了并发事务之间能看到什么、会被阻塞多久。资源包如果演示并发场景,建议显式设置隔离级别而不是依赖默认值,这样换环境结果才一致。

隔离级别脏读不可重复读幻读常见用途
READ UNCOMMITTED可能可能可能演示脏读
READ COMMITTED否可能可能多数业务默认
REPEATABLE READ否否可能MySQL 默认
SERIALIZABLE否否否强一致场景

设置方式:MySQL 用SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;,SQL Server 用SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;。锁等待超时也要留意,MySQL 默认innodb_lock_wait_timeout是 50 秒,演示死锁时等 50 秒太磨人,可以临时调小到 5 秒:SET SESSION innodb_lock_wait_timeout = 5;。调小之后死锁会更快报错,方便观察。

4.2 触发器递归与嵌套的开关

触发器里如果对触发它的同一张表做写操作,可能引发递归触发,轻则日志重复,重则栈溢出。MySQL 默认recursion_depth有限制,SQL Server 有RECURSIVE_TRIGGERS开关。资源包里如果有“更新订单状态时写日志,日志表又关联订单表”这种设计,跑之前先确认递归开关状态。

-- MySQL 查看当前递归深度限制 SHOW VARIABLES LIKE 'max_sp_recursion_depth'; -- SQL Server 关闭递归触发器(数据库级) ALTER DATABASE db_lab5 SET RECURSIVE_TRIGGERS OFF;

建议实验环境里默认关闭递归触发器,需要演示递归时再单独打开。关闭后如果业务逻辑依赖递归触发,会表现为“日志只写了一条”,这时候别急着怀疑脚本写错,先查开关。

4.3 存储过程异常处理的兜底写法

存储过程里如果只写业务逻辑不写异常处理,一旦中间某条语句报错,事务可能悬空不提交也不回滚,连接断开后才被动回滚,实验里表现为“数据时对时不对”。稳妥写法是加EXIT HANDLER(MySQL)或TRY...CATCH(SQL Server)兜底。

-- SQL Server 的 TRY...CATCH 兜底写法 CREATE PROCEDURE place_order_sqlserver @product_id INT, @quantity INT, @result INT OUTPUT AS BEGIN SET @result = 0; BEGIN TRY BEGIN TRANSACTION; UPDATE product SET stock = stock - @quantity WHERE product_id = @product_id AND stock >= @quantity; IF @@ROWCOUNT = 0 THROW 50001, '库存不足', 1; INSERT INTO order_detail (order_id, product_id, quantity) VALUES (FLOOR(RAND() * 100000), @product_id, @quantity); COMMIT TRANSACTION; SET @result = 1; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; SET @result = 0; END CATCH; END;

这段的关键是BEGIN CATCH里先判断@@TRANCOUNT > 0再回滚,避免“没有活动事务却回滚”的二次报错。THROW用来主动抛异常,让CATCH接住,比用RAISERROR更清晰。参数上@result在过程开头先置 0,保证任何路径都有返回值,不会出现 NULL。

5. 避坑排查:实验五最容易翻车的五个地方

实验五的报错往往不直观,同一个现象可能对应好几种原因。下面这五条是按“现象 → 原因 → 解决”整理的踩坑记录,基本覆盖了资源包跑不通时的高频问题。

5.1 事务回滚了但数据还在

现象:脚本里明明写了ROLLBACK,查表发现数据还是被改了。原因通常是回滚之前已经隐式提交,比如 MySQL 里执行了CREATE TABLE、ALTER TABLE这类 DDL 语句会隐式提交当前事务,或者用了不支持事务的存储引擎(MyISAM)。解决:确认表引擎是 InnoDB(SHOW TABLE STATUS;看 Engine 列),把 DDL 挪到事务外,回滚前不要夹带任何隐式提交语句。

5.2 存储过程调用报“参数数量不匹配”

现象:CALL place_order(1001, 2);报参数个数错误。原因:过程定义里有OUT参数,调用时必须传一个变量占位,不能省略。解决:先SET @r = 0;再CALL place_order(1001, 2, @r);,SQL Server 则要加OUTPUT关键字。这个坑新手几乎必踩,因为IN参数可以省略不传(有默认值时),OUT参数不行。

5.3 触发器不触发

现象:插入数据后审计表没记录。原因有三种常见情况:触发器建在了错误的表上、触发时机写成了AFTER但业务需要BEFORE、或者插入操作本身因为约束失败没执行成功。解决:先SHOW TRIGGERS;确认触发器存在且绑定表正确,再单独跑一条最简单的INSERT看是否成功,最后检查触发时机。如果插入被外键约束挡了,触发器自然不会触发。

5.4 死锁报错但复现不了

现象:实验要求演示死锁,但跑脚本时有时报有时不报。原因:死锁依赖两个事务的加锁顺序交叉,单会话顺序执行永远复现不了,必须两个会话并发且加锁顺序相反。解决:开两个连接,会话 A 先锁表 1 再锁表 2,会话 B 先锁表 2 再锁表 1,中间加SELECT ... FOR UPDATE或UPDATE制造排他锁,就能稳定复现。注意锁等待超时要调小,否则等太久。

5.5 中文乱码或日期格式对不上

现象:插入的中文变成问号,或者日期报“格式不正确”。原因:库或表的字符集不是utf8mb4,或者日期字符串格式与数据库期望的YYYY-MM-DD不一致。解决:建库时显式指定字符集,连接串加charset=utf8mb4;日期统一用标准格式,别用2024/1/1这种斜杠写法,MySQL 严格模式下会直接报错。

6. 进阶验证:用执行计划和锁信息把实验做透

脚本跑通只是及格线,想把实验五做出深度,得学会看执行计划和锁信息。资源包里的查询如果没走索引,事务持有锁的时间会变长,并发实验的结果就不稳定。我一般会在跑事务实验前先看一眼执行计划,确认UPDATE和SELECT都走了索引,再开并发。

-- MySQL 查看执行计划,重点看 type 和 rows EXPLAIN SELECT * FROM product WHERE product_id = 1001; -- 查看当前锁等待情况(MySQL 8.0) SELECT * FROM performance_schema.data_lock_waits; -- SQL Server 查看锁信息 EXEC sp_lock;

EXPLAIN输出里type是ALL说明全表扫描,事务更新时会锁住更多行,并发实验容易互相阻塞;改成ref或eq_ref说明走了索引,锁范围小,结果更可控。data_lock_waits能看到谁在等谁的锁,演示死锁时用它佐证比只看报错更有说服力。参数上performance_schema需要开启,MySQL 8.0 默认开,5.7 可能要手动开。

另一个进阶技巧是把实验五的存储过程改造成带版本号的审计方案:每次更新订单状态时,不覆盖原记录,而是往历史表插一条新版本,配合触发器自动记录变更前后值。这样既练了触发器,又练了事务,还能在实验报告里体现“可追溯”的设计思路。具体做法是在AFTER UPDATE触发器里用OLD和NEW关键字取变更前后的值:

CREATE TRIGGER trg_order_history AFTER UPDATE ON order_detail FOR EACH ROW BEGIN INSERT INTO order_history (order_id, old_quantity, new_quantity, change_time) VALUES (NEW.order_id, OLD.quantity, NEW.quantity, NOW()); END;

OLD.quantity是更新前的值,NEW.quantity是更新后的值,两者都只在行级触发器里可用。这个写法在实验报告里能加分,因为它同时体现了触发器、审计和事务一致性三个知识点。

从那以后我每次跑数据库实验,都会先EXPLAIN看一眼执行计划再开事务,确认索引走了、锁范围可控,才敢下并发结论。希望这份拆解帮到你,把实验五从“能跑”做到“跑得明白”。

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

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

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

立即咨询