Kodewick
第 20 课,共 21 课目录

事务

第 20 课,共 21 课 · 第 5 部分 · 改数据和建表 · 约 15 分钟

有的事要改好几处才算做完。客户在网店下一单,数据库里要改三处:orders 里加一个订单,order_items 里加买了什么,products 里减库存。

main.sql
1INSERT INTO orders (id, customer_id, order_date, status)2VALUES (13, 10, '2025-10-28', '待发货');3INSERT INTO order_items (order_id, product_id, quantity, unit_price)4VALUES (13, 5, 1, 59.00);5UPDATE products SET stock = stock - 1 WHERE id = 5;6 7SELECT id, name, stock FROM products WHERE id = 5;
输出
第 1 行的语句
Query OK,影响了 1 行
第 3 行的语句
Query OK,影响了 1 行
第 5 行的语句
Query OK,影响了 1 行 · Rows matched: 1 Changed: 1 Warnings: 0
第 7 行的语句
idnamestock
5耳机9
1 行

三条都成功了,耳机的库存从 10 变成 9。可要是只成功了一半呢?

改到一半出错了

这次第二条写错了商品编号,没有 99 号商品:

main.sql
1INSERT INTO orders (id, customer_id, order_date, status)2VALUES (13, 10, '2025-10-28', '待发货');3INSERT INTO order_items (order_id, product_id, quantity, unit_price)4VALUES (13, 99, 1, 59.00);5UPDATE products SET stock = stock - 1 WHERE id = 5;
输出
Query OK,影响了 1 行
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`town`.`order_items`, CONSTRAINT `order_items_ibfk_2` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`))

「product_id」的值 99 在表「products」里找不到:外键要指向一行已经有的数据。先往「products」里插入它,或者改成已有的值。

第二条报错,后面的也没跑。可第一条已经成功了:数据库里多了一个 13 号订单,里面什么也没买。这种只改了一半的数据,比不改还糟:客户能看到一个空订单,报表里的订单数也多了一个。

我们想要的是:这三条要么全成功,要么一条都不算。这就是事务。

START TRANSACTION 和 COMMIT

START TRANSACTION 开始一个事务。之后的修改先「记着」,到 COMMIT(提交)才真正生效:

main.sql
1START TRANSACTION;2INSERT INTO orders (id, customer_id, order_date, status)3VALUES (13, 10, '2025-10-28', '待发货');4INSERT INTO order_items (order_id, product_id, quantity, unit_price)5VALUES (13, 5, 1, 59.00);6UPDATE products SET stock = stock - 1 WHERE id = 5;7COMMIT;8 9SELECT id, customer_id, status FROM orders WHERE id = 13;
输出
第 1 行的语句
Query OK,影响了 0 行
第 2 行的语句
Query OK,影响了 1 行
第 4 行的语句
Query OK,影响了 1 行
第 6 行的语句
Query OK,影响了 1 行 · Rows matched: 1 Changed: 1 Warnings: 0
第 7 行的语句
Query OK,影响了 0 行
第 9 行的语句
idcustomer_idstatus
1310待发货
1 行

和第一段的结果一样。区别在出事的时候:COMMIT 之前,这些修改都还可以撤回。

ROLLBACK:撤回

ROLLBACK(回滚)撤回事务里所有还没提交的修改,数据回到 START TRANSACTION 之前的样子。有人要买 20 副耳机,可库存只有 10:

main.sql
1START TRANSACTION;2UPDATE products SET stock = stock - 20 WHERE id = 5;3SELECT name, stock FROM products WHERE id = 5;4 5ROLLBACK;6SELECT name, stock FROM products WHERE id = 5;
输出
第 1 行的语句
Query OK,影响了 0 行
第 2 行的语句
Query OK,影响了 1 行 · Rows matched: 1 Changed: 1 Warnings: 0
第 3 行的语句
namestock
耳机-10
1 行
第 5 行的语句
Query OK,影响了 0 行
第 6 行的语句
namestock
耳机10
1 行

减完一看,库存成了 -10,不对。ROLLBACK 之后,库存又是 10,就像那条 UPDATE 从没跑过。点单步,一条一条看库存怎么变。

先猜一猜

最后查出来是几?

1START TRANSACTION;2DELETE FROM order_items WHERE order_id = 4;3DELETE FROM orders WHERE id = 4;4ROLLBACK;5SELECT COUNT(*) FROM orders;

转账:两条 UPDATE

讲事务最常用的例子是转账:A 给 B 转 100 元,A 的余额减 100,B 的余额加 100。只成功了第一条,100 元就凭空没了。两条 UPDATE 必须一起成功。

网店里也有这样的事。7 号订单还没发货,客户要取消:订单状态改成「已取消」,买的双肩包(1 个)和帆布袋(2 个)放回库存。三条一起:

main.sql
1START TRANSACTION;2UPDATE orders SET status = '已取消' WHERE id = 7;3UPDATE products SET stock = stock + 1 WHERE id = 3;4UPDATE products SET stock = stock + 2 WHERE id = 10;5COMMIT;6 7SELECT id, name, stock FROM products WHERE id IN (3, 10);
输出
第 1 行的语句
Query OK,影响了 0 行
第 2 行的语句
Query OK,影响了 1 行 · Rows matched: 1 Changed: 1 Warnings: 0
第 3 行的语句
Query OK,影响了 1 行 · Rows matched: 1 Changed: 1 Warnings: 0
第 4 行的语句
Query OK,影响了 1 行 · Rows matched: 1 Changed: 1 Warnings: 0
第 5 行的语句
Query OK,影响了 0 行
第 7 行的语句
idnamestock
3双肩包16
10帆布袋27
2 行

出错就撤回

一条语句出错时,数据库只是不执行这一条,前面已经做了的修改并不会自己撤回。所以真正的程序(比如网店后台)都是这样写的:

  1. START TRANSACTION;
  2. 一条一条执行;
  3. 全都成功,就 COMMIT;中间任何一条出错,就 ROLLBACK。

这个页面遇到错误就停下,不会帮你写第 3 步。比如下面这段停在第二条,没有 COMMIT 的事务里的修改,最后都不会生效:

main.sql
1START TRANSACTION;2INSERT INTO orders (id, customer_id, order_date, status)3VALUES (13, 10, '2025-10-28', '待发货');4INSERT INTO order_items (order_id, product_id, quantity, unit_price)5VALUES (13, 99, 1, 59.00);
输出
第 1 行的语句
Query OK,影响了 0 行
第 2 行的语句
Query OK,影响了 1 行
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`town`.`order_items`, CONSTRAINT `order_items_ibfk_2` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`))

「product_id」的值 99 在表「products」里找不到:外键要指向一行已经有的数据。先往「products」里插入它,或者改成已有的值。

不写 START TRANSACTION 的时候

不开事务时,MySQL 把每一条语句都当作一个自己的小事务,执行完马上提交(叫「自动提交」)。所以这时候 ROLLBACK 什么也撤不回:

main.sql
1UPDATE products SET price = 0 WHERE id = 5;2ROLLBACK;3 4SELECT name, price FROM products WHERE id = 5;
输出
第 1 行的语句
Query OK,影响了 1 行 · Rows matched: 1 Changed: 1 Warnings: 0
第 2 行的语句
Query OK,影响了 0 行
第 4 行的语句
nameprice
耳机0.00
1 行

价格还是 0。想撤回,必须先 START TRANSACTION。

注意: CREATE TABLE、ALTER TABLE、DROP TABLE 这些改表结构的语句,在 MySQL 里会先把正在进行的事务自动提交掉,所以它们撤不回。别的数据库(比如 PostgreSQL)可以撤回。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

下一单

10 号客户买 2 个咖啡杯(8 号商品,单价 9.90)。在一个事务里:插入 13 号订单(2025-10-28,「待发货」)、插入它的明细、咖啡杯库存减 2,然后提交。最后的查询不用改。

要显示
idnameproductquantitystock
13郑雪咖啡杯258
1 行
exercise1.sql
1SELECT o.id, c.name, p.name AS product, oi.quantity, p.stock2FROM orders AS o3JOIN customers AS c ON c.id = o.customer_id4JOIN order_items AS oi ON oi.order_id = o.id5JOIN products AS p ON p.id = oi.product_id6WHERE o.id = 13;
输出
点「运行」或按 Ctrl+Enter
练习 2

改错了

本来只想给笔记本涨价,UPDATE 却忘了写 WHERE,所有价格都翻了一倍。好在开了事务:别删掉那条 UPDATE,在它和查询之间撤回它,让查询显示原来的价格。

要显示
nameprice
笔记本4.50
中性笔(10 支)6.00
2 行
exercise2.sql
1START TRANSACTION;2UPDATE products SET price = price * 2;3 4SELECT name, price5FROM products6WHERE category = '文具'7ORDER BY id;
输出
点「运行」或按 Ctrl+Enter
练习 3

取消订单

12 号订单(4 个咖啡杯,8 号商品)要取消。在一个事务里:把它的 status 改成「已取消」,把 4 个咖啡杯放回库存,然后提交。最后查出 12 号订单的 status 和咖啡杯的 stock,列名就叫 status 和 stock。

要显示
statusstock
已取消64
1 行
exercise3.sql
1SELECT o.status, p.stock2FROM orders AS o3JOIN order_items AS oi ON oi.order_id = o.id4JOIN products AS p ON p.id = oi.product_id5WHERE o.id = 12;
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 事务把几条修改绑在一起:要么全部生效,要么全部撤回。
  • START TRANSACTION 开始;COMMIT 提交,修改才真正生效;ROLLBACK 撤回还没提交的所有修改。
  • 一条语句出错,只是这一条不执行,前面的不会自己撤回:程序里出错就要 ROLLBACK。
  • 下单、取消订单、转账这种「几处一起改」的事,都应该放进事务。
  • 不开事务时每条语句自动提交,ROLLBACK 撤不回;MySQL 里 CREATE、ALTER、DROP 也撤不回。