简介:西北工业大学数据库实验报告5是一份面向数据库课程学习者与备考学生的实验文档,内容围绕数据表的创建与管理、存储过程的编写与执行、触发器的设计与验证展开。报告基于数据库环境,完整还原了典型上机实验:使用系统存储过程重命名视图,创建带参数存储过程和加密存储过程,查看与删除存储过程,创建插入、删除、更新等多种触发器,并设计了自动维护成绩统计表的级联触发方案。每个实验都包含实验要求、关键语句和验证结果,便于对照练习和排查问题。压缩包内仅有一个文档文件,类型为Word,大小约两百三十一KB,该资源已有七百余人浏览学习。报告既适合正在学习数据库原理的学生作为实验参考,也可供备考或复习数据库操作技能时快速查阅,通过这套实验报告,读者可以系统掌握数据库对象从创建到应用的完整流程,尤其是触发器在不同操作场景下的实际作用,为后续课程设计与工程实践打下基础。
1. 数据库实验5:为什么说是从“会查”到“会写程序”的分水岭
数据库实验5是一个很有意思的节点。在实验1到实验4里,你做的事情可以概括为一句话:让 SQL 具备从建库、增删改查到基础查询的完整能力。到了实验5,绝大多数学校会把题目从“查询正确”切换成“逻辑正确”,具体落点通常是存储过程与触发器。不同学校在题目里用的数据库可能差别很大,MySQL、Oracle 还是达梦数据库都有人用,但核心一致——你必须把一段完整业务流程写进数据库内部,让它自己判断库存够不够、金额对不对、日志记没记。
这个转变会立刻击碎一个幻觉:靠肉眼判断“数据对不对”不再适用。存储过程要同时完成校验、计算、回写和异常处理;触发器要在用户察觉不到的瞬间把约束和审计固化进数据库。数据库课程设计里那些数据不一致的坑,很多就源于实验5没把这两类对象吃透。下面用一个带订单、库存、审计日志的场景,把存储过程和触发器从设计、实现、调试到报告验证完整走一遍。示例用 MySQL 做实现,但参数设计与流程控制在其他数据库里同样成立。
2. 实验5的题目拆解:业务规则该下沉到数据库的哪一层
2.1 为什么业务规则要下沉到数据库
先从数据库原理的层面回答一个前置问题:实验5为什么通常不允许只写应用层代码去校验,而非要在数据库内部完成?最直接的原因是复用性。同一个数据库会被多个客户端连接,Java、Python、命令行工具各写一套逻辑,就会出现“在一个客户端里禁止超卖、在另一个客户端里却放行”的尴尬局面。把规则放进存储过程和触发器之后,所有入口都走同一套校验,这是实验5训练的核心思维。
第二个原因是事务边界。一段业务如果先更新订单表再扣减库存,这两步必须在一个事务里提交或回滚。应用层把两步分开后,中间任何一步失败,数据就处在不一致状态;而存储过程内部可以用 START TRANSACTION 和 COMMIT 把边界锁死,由数据库保证原子性。对 MySQL 实验而言,InnoDB 的行锁、外键和事务日志都服务于这个目标。
第三个原因是触发器的不可替代性:有些约束是建表语句表达不了的。“订单数量被修改后必须自动记录变更前后值”这类需求,CHECK 约束做不到,必须靠触发器在事件发生后自动执行。把这三条写进实验报告引言,说明你是带着目的做的,而不是照着示例抄了一遍。
2.2 存储过程和触发器的边界:一个管流程、一个管事件
存储过程是显式调用的对象,支持 IN、OUT、INOUT 参数,可以在内部自由控制事务边界,还能把结果集返回给调用方,因此天然适合做“流程”。触发器是隐式调用的对象,由 INSERT、UPDATE、DELETE 事件自动触发,不接收参数,也不能向调用方返回值,因此天然适合做“规则”。
实验5里最常见的误用,是拿触发器去模拟存储过程的业务流程,比如在触发器里逐行更新多张表并处理复杂分支。这样做的后果是:一次普通的 UPDATE 会引爆一串隐式动作,客户端看到的错误往往来自最内层的触发器,排查时根本不知道该断在哪一层。正确划分是:需要被多个客户端显式调用的完整事务流程放进存储过程;需要在数据变更瞬间自动生效的约束与审计交给触发器。如果题目要求的是一个纯流程操作,比如“生成订单并扣减库存”,就尽量全部由存储过程完成,不要让触发器在里面掺一脚。
2.3 三方案选择矩阵:存储过程、触发器与应用层代码
| 维度 | 存储过程 | 触发器 | 应用层代码 |
|---|---|---|---|
| 调用方式 | 显式 CALL | DML 事件隐式触发 | 调用方显式触发 |
| 参数支持 | IN / OUT / INOUT | 仅 NEW / OLD 伪行 | 自由定义 |
| 返回值 | 支持结果集 | 不支持 | 自由 |
| 事务控制 | 可 COMMIT / ROLLBACK | 跟随触发语句所在事务 | 取决于连接配置 |
| 适用场景 | 业务流程、报表计算 | 审计、约束、级联更新 | 复杂算法、展示逻辑 |
| 排障难度 | 低,可单步调用 | 高,隐式执行 | 中等,可打日志 |
这张表可以直接放进实验报告“方案设计”一节,用来交代“为什么选触发器而不是存储过程”。实际工程中,审计和约束用触发器,核心业务流程用存储过程,复杂算法留在应用层。实验报告里写出这个结论,答辩时基本不需要再被追问技术选型。
3. 用 MySQL 跑通实验5的最小实现:存储过程与触发器全流程
3.1 建表与初始化:三张核心表的结构设计
实验5一般基于一个小型业务库展开,这里用商品、订单、订单日志三张表演示。先建立数据库和表结构:
CREATE DATABASE IF NOT EXISTS db_exp5 DEFAULT CHARSET utf8mb4; USE db_exp5; -- 商品表:保存价格与当前库存 CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, price DECIMAL(10, 2) NOT NULL, stock INT NOT NULL DEFAULT 0 ); -- 订单表:保存每次购买的明细 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, product_id INT NOT NULL, qty INT NOT NULL, total DECIMAL(10, 2) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 订单日志表:由触发器写入,记录更新痕迹 CREATE TABLE order_log ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, action VARCHAR(10) NOT NULL, before_qty INT, after_qty INT, before_total DECIMAL(10, 2), after_total DECIMAL(10, 2), op_time DATETIME DEFAULT CURRENT_TIMESTAMP ); INSERT INTO product (name, price, stock) VALUES ('数据库原理教材', 59.90, 100), ('实验指导书', 35.00, 50);建表细节会影响实验5的后续效果。价格和金额用 DECIMAL 而不是 FLOAT,避免浮点误差在累计金额后失控;库存用 INT 且默认 0,保证未初始化商品不会意外出售;订单日志表单独拆分,是为了把“审计动作”从业务主表中独立出来,方便查看触发器的写入结果。
3.2 写存储过程:下单事务的参数设计与事务控制
接下来实现核心的下单存储过程,完整代码如下:
DELIMITER // CREATE PROCEDURE sp_create_order( IN p_product_id INT, IN p_qty INT, OUT p_order_id INT ) BEGIN DECLARE v_price DECIMAL(10, 2) DEFAULT NULL; DECLARE v_stock INT DEFAULT NULL; DECLARE v_total DECIMAL(10, 2); START TRANSACTION; -- 加排他锁读取商品行,防止并发修改库存 SELECT price, stock INTO v_price, v_stock FROM product WHERE id = p_product_id FOR UPDATE; IF v_stock IS NULL THEN ROLLBACK; SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'product_not_found'; END IF; IF p_qty <= 0 THEN ROLLBACK; SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'quantity_invalid'; END IF; IF v_stock < p_qty THEN ROLLBACK; SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'stock_not_enough'; END IF; SELECT ROUND(v_price * p_qty, 2) INTO v_total; INSERT INTO orders (product_id, qty, total) VALUES (p_product_id, p_qty, v_total); UPDATE product SET stock = stock - p_qty WHERE id = p_product_id; COMMIT; SET p_order_id = LAST_INSERT_ID(); END // DELIMITER ;代码里有几个必须写进实验报告的要点。第一,SELECT ... FOR UPDATE对商品行加排他锁,这是后面讨论并发与死锁的基础;第二,SIGNAL SQLSTATE '45000'是 MySQL 5.6 之后推荐的错误上报方式,客户端会直接收到带文本的异常,比在过程里 SELECT 一个错误标记再手工判断可靠得多;第三,三段校验逻辑放在同一个事务内,配合 ROLLBACK 才能实现“不成功即无痕”。DECLARE变量初始化为 NULL,是为了让产品不存在的场景能被IS NULL分支精确捕获。OUT 参数p_order_id在事务提交后才赋值,避免调用方拿到尚未提交的 ID。
3.3 写触发器:订单审计的 AFTER UPDATE 实现
审计类需求用 AFTER UPDATE 触发器实现:
DELIMITER // CREATE TRIGGER trg_orders_audit AFTER UPDATE ON orders FOR EACH ROW BEGIN INSERT INTO order_log (order_id, action, before_qty, after_qty, before_total, after_total) VALUES (OLD.id, 'UPDATE', OLD.qty, NEW.qty, OLD.total, NEW.total); END // DELIMITER ;这里用 AFTER UPDATE 而不是 BEFORE UPDATE,因为审计要记录“更新完成后的实际事实”;BEFORE 阶段 NEW 值尚未真正写入,极端场景下日志与实际数据会出现偏差。OLD 和 NEW 伪行提供了变更前后的完整镜像,这也是触发器比存储过程处理历史轨迹更方便的地方:存储过程需要先查一次旧值,触发器直接就能拿到。
3.4 调用、验证与错误注入:实验报告要留的三个证据
完成存储过程和触发器后,需要在实验报告里保留三组可复现的执行证据:
-- 证据1:正常调用,验证订单生成、库存扣减 CALL sp_create_order(1, 2, @oid); SELECT @oid; SELECT * FROM orders WHERE id = @oid; SELECT * FROM product WHERE id = 1; SELECT * FROM order_log; -- 证据2:绕过存储过程直接改订单,验证触发器生效 UPDATE orders SET qty = 3, total = 179.70 WHERE id = @oid; SELECT * FROM order_log WHERE order_id = @oid; -- 证据3:注入库存不足,验证事务回滚且无残留 CALL sp_create_order(1, 9999, @oid2); SELECT @oid2; SELECT * FROM orders; SELECT * FROM product WHERE id = 1;执行证据1后,商品1的库存从 100 变成 98,订单表出现一条 total 为 119.80 的记录,此时 order_log 为空。执行证据2后,order_log 里出现一条 action 为 UPDATE 的记录,before_qty 为 2、after_qty 为 3。这里故意用一条手工 UPDATE 模拟“外部未经校验的客户端”,要说明的正是:只有触发器能在所有入口统一拦截这种变更。执行证据3会抛出stock_not_enough异常,随后查询 orders 和 product,确认没有残留的半条数据。第三组证据是验证事务整体性的核心依据,比单纯截图“操作成功”有说服力得多。
4. 实验报告里最容易被追问的四个雷区
4.1 回滚之后,触发器到底还写不写日志
答辩环节几乎必问一个问题:存储过程里触发 ROLLBACK 时,前面由触发器写入的 order_log 会被保留还是回滚?答案是一起回滚。因为 order_log 与 orders、product 处于同一个事务上下文,触发器并不具备独立提交事务的能力,它只是事务里的一次普通写入。所以,如果审计需求要求“业务回滚后也留下记录”,用 AFTER 触发器是做不到的。
提示:需要保留回滚前审计痕迹时,可以把日志表改为 MyISAM 引擎,或把审计动作放到独立连接中执行,但后者与存储过程的单线程模型冲突,实际工程里更推荐应用层在调用前先落一条“准备日志”。实验报告里把这一点讲清楚,属于加分项。
4.2 并发下单时为什么会死锁:用实验证据说明你调过
两个会话同时对同一商品执行 sp_create_order,会因为SELECT ... FOR UPDATE产生锁等待。更麻烦的是多商品订单场景:会话 A 锁定商品 1 再申请商品 2,会话 B 锁定商品 2 再申请商品 1,就形成循环等待。MySQL 检测到死锁后会自动回滚其中一个事务,另一个继续执行,客户端会看到Deadlock found when trying to get lock的错误。
排查时不要只看应用日志。MySQL 8.0 下执行SHOW ENGINE INNODB STATUS\G,重点读LATEST DETECTED DEADLOCK段落,里面会明确写出两个事务分别持有的锁和正在等待的锁。这条命令要放在 mysql 命令行客户端会话里执行,放进 Navicat 查询窗口会直接语法报错。实验报告里贴这段截图,并指出“按固定顺序加锁”“缩小锁范围”两种解法,这一题基本能拿满。
4.3 SIGNAL 与返回码:报错别只靠 SELECT
很多初版实验把错误处理写成SELECT '库存不足';,问题在于它只把提示放进结果集里,调用方要额外封装才能拿到,而且不会中断过程,后面的插入和扣减照常执行。改用SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'stock_not_enough',可以同时做到两件事:向客户端抛出可见异常,并立刻终止存储过程。实验报告里把两种写法都保留,对比运行结果,能直接体现调试深度。
4.4 触发器递归与级联:小心一条 UPDATE 引爆一串动作
MySQL 触发器默认不具备同表递归触发能力,但跨表触发器链是真实存在的:更新 orders 时另一个 AFTER UPDATE 触发器又更新 product,而 product 上的触发器又反过来更新 orders,就会形成触发环。实验5里最常见的坑不是数据错,而是“不知道触发器在哪一层断了”。
排查办法是把库里的触发器定义全部拉出来,确认触发关系:
SELECT TRIGGER_SCHEMA, TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE, ACTION_STATEMENT FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = 'db_exp5'\G不管题目多复杂,触发器数量控制在三张表以内,只做“写日志、改快照、简单校验”三件事,不在触发器里调用存储过程,触发级联混乱就基本不会遇到。
5. 收尾技巧:用 SHOW CREATE 把定义导出为实验报告的有效附录
实验报告附录常被忽视,但它决定了老师快速判断报告含金量的效率。与其贴十几张零散的查询截图,不如在附录里放一段可回放的定义导出。MySQL 的 SHOW CREATE 系列命令能精确还原每个对象的创建语句:
SHOW CREATE PROCEDURE sp_create_order\G SHOW CREATE TRIGGER trg_orders_audit\G把两条命令的输出整理进报告,配一句“以上定义由 MySQL 8.0 直接导出,未做修改”,可信度会明显提升。.doc 报告里 SQL 一定要保持为可复制的文本,不能转成图片;在 Word 里为代码段设置 Consolas 或 Courier New 等宽字体,字号小四,行距固定值 20 磅,避免打印后折行。
再补一个技巧:把存储过程的外部调用代码也放进附录,证明你验证过“外部程序连接数据库并调用存储过程”的完整链路:
import mysql.connector conn = mysql.connector.connect( host="127.0.0.1", user="root", password="your_password", database="db_exp5" ) cursor = conn.cursor() cursor.callproc("sp_create_order", [1, 2, None]) # MySQL Connector/Python 会把 OUT 参数包装成结果集返回 for result in cursor.stored_results(): row = result.fetchone() if row: print("order_id:", row[0])这段代码的说明里要解释一个很多人踩过的坑:MySQL Connector/Python 的 callproc 会把 OUT 参数放在存储过程的结果集里,cursor.lastrowid并不等于订单 ID,它只代表连接上最后一次自动递增操作的值。存储过程内部已经执行了 COMMIT,调用方不需要再 commit,但必须通过stored_results()读取 OUT 参数,否则连接会在下一次操作时把残留结果集当成错误抛出。把这段 Python 代码和前面的 SHOW CREATE 输出放在一起,报告的验证闭环就完整了,老师按命令重放一遍就能复现全部结果。
本文还有配套的精品资源,点击获取