
简介西北工业大学数据库实验报告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 三方案选择矩阵存储过程、触发器与应用层代码维度存储过程触发器应用层代码调用方式显式 CALLDML 事件隐式触发调用方显式触发参数支持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( host127.0.0.1, userroot, passwordyour_password, databasedb_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 输出放在一起报告的验证闭环就完整了老师按命令重放一遍就能复现全部结果。本文还有配套的精品资源点击获取