首页 / 资讯中心 / 文章详情

【仓颉语言入门 · 第二篇 第 13 课】

【仓颉语言入门 · 第二篇 第 13 课】 ★ FEATURED ARTICLE
【仓颉语言入门 · 第二篇 第 13 课】事务批量操作的成败一致上一课结尾埋了个问题一条业务操作往往要动好几张表——转账要一扣一加缺一半都不算数。可我们写出来的每条 SQL 都是独立执行的第二条失败时第一条已经落库数据就烂在一半。这一课就把这个坑填上先在客户端和代码里各制造一次钱没了的事故再请出本课主角事务把一组 SQL 打包成要么全成、要么全不算的整体。目录第二篇导航第一篇《仓颉语言入门》共 30 课已完结环境搭建 → 语法 → 函数 → 类型系统 → 工程化 → 并发 → 两场实战。第二篇在更高的起点上继续共26 课、4 个模块模块课次内容一、补遗篇0106正则、时间日期、命令行参数、随机数、加密哈希、二进制处理二、数据库篇0715入门与连接、建表、查询、插入、更新删除、预编译防注入、事务、手写 DAO、综合小实战三、进阶硬核篇1622反射、宏基础、deriving 实战、FFI 基础、FFI 进阶、并发容器、对象池四、实战项目篇2326HTTP 基础、HTTP 进阶、多线程日志分析器、简易爬虫正则表达式std.regex时间日期处理std.time命令行参数解析std.argopt随机数与概率std.random加密与哈希std.crypto二进制数据处理std.binary数据库入门认识 std.database.sql连上 MySQL建表用代码定义表结构查询SELECT 与结果集遍历插入INSERT、占位符与自增主键更新与删除UPDATE / DELETE预编译与防 SQL 注入事务批量操作的成败一致本文手写 DAO 数据访问层数据库综合小实战反射在运行时打量类型宏基础std.ast 与 quotederiving 宏实战与 C 互操作基础FFI 调用 C 函数FFI 进阶结构体、指针与编译配置并发容器并发安全的集合对象池与资源复用实战三HTTP 服务端基础实战四HTTP 进阶与 JSON 接口实战五多线程日志分析器实战六简易爬虫一、事故现场钱扣了人没收到本课全程用一张新表account账户表讲故事。老规矩先在客户端里把它建好灌入两个账户各 1000 元USEtumo_governance;DROPTABLEIFEXISTSaccount;CREATETABLEaccount(idBIGINTPRIMARYKEYAUTO_INCREMENT,nameVARCHAR(50)NOTNULL,balanceBIGINTNOTNULL);INSERTINTOaccount(name,balance)VALUES(zhangsan,1000),(lisi,1000);SELECT*FROMaccount;----------------------- | id | name | balance | ----------------------- | 1 | zhangsan | 1000 | | 2 | lisi | 1000 | -----------------------现在来一笔转账zhangsan 给 lisi 转 100 元。拆成 SQL 就是两条 UPDATE一条扣、一条加UPDATEaccountSETbalancebalance-100WHEREnamezhangsan;UPDATEaccountSETbalancebalance100WHEREnamelisi;两条都成功皆大欢喜。但假如第二条语句出了岔子呢我们故意把第二条的列名写错balance2模拟第二条 SQL 执行失败UPDATEaccountSETbalancebalance-100WHEREnamezhangsan;Query OK, 1 row affected Rows matched: 1 Changed: 1 Warnings: 0UPDATEaccountSETbalance2balance100WHEREnamelisi;ERROR 1054 (42S22): Unknown column balance2 in field list第一条成功第二条报错。查一下余额SELECT*FROMaccount;----------------------- | id | name | balance | ----------------------- | 1 | zhangsan | 900 | | 2 | lisi | 1000 | -----------------------zhangsan 的 100 块扣掉了lisi 却没收到——100 元凭空蒸发。为什么会这样因为 MySQL 默认处于自动提交模式每条 SQL 一执行完就立刻落库、立刻生效各管各的。第一条 UPDATE 已经签字画押第二条的失败根本牵连不到它。而转账这件事的本质要求是扣款和加款要么都成功要么都不算数。这正是事务要解决的问题。二、客户端里把事务三件套试明白事务Transaction就是把多条 SQL 打包成一个整体打包期间执行的改动都只是草稿最后你喊一声定稿提交才真正落库中间出了任何问题喊一声作废回滚草稿全部撕掉数据回到打包前的样子。MySQL 客户端里的三件套语句作用START TRANSACTION;开始一个事务之后的改动都是草稿COMMIT;提交草稿定稿全部落库ROLLBACK;回滚草稿作废全部撤销三、先在代码里复现一次事故客户端明白了搬进仓颉代码。先用我们熟悉的老写法不开事务复现一次事故——两条 UPDATE 各自独立执行第二条故意写错列名package cj_mysql_demo import std.database.sql.* import mariadb.cdbc; main(): Int64 { let driver DriverManager.getDriver(mariadb).getOrThrow() let ds driver.open(mariadb://127.0.0.1:3306, [ (username, root), (password, wangpeng), (database, tumo_governance) ]) let conn ds.connect() // 演示前先把两人的余额重置为 1000 let reset conn.prepareStatement(UPDATE account SET balance 1000) reset.update() reset.close() let stmt1 conn.prepareStatement(UPDATE account SET balance balance - 100 WHERE name zhangsan) stmt1.update() println(zhangsan 扣款成功) stmt1.close() let stmt2 conn.prepareStatement(UPDATE account SET balance2 balance 100 WHERE name lisi) stmt2.update() println(lisi 加款成功) stmt2.close() conn.close() 0 }运行输出zhangsan 扣款成功 An exception has occurred: SqlException: Unknown column balance2 in field list, errorCode: 0zhangsan 扣款成功打印出来了随后程序在第二条语句处抛异常退出。用客户端查余额和第一节一模一样的惨状zhangsan 900、lisi 1000。代码里的每条update()和客户端一样执行完就独立提交了。这里顺带说一个本课实测的细节异常是在prepareStatement那一行就抛出的不是update()才抛——mariadb 驱动在 prepare 阶段就会把 SQL 发给服务器校验列名不对当场报错。所以上面对事故现场的代码顺序做了对应安排先执行完第一条再去 prepare 那条错误的语句。四、事务版createTransaction 三件套仓颉侧的事务 API 和客户端三件套一一对应只是事务要先从连接上领出来客户端 SQL仓颉代码作用连接自动提交中let tx conn.createTransaction()从连接创建一个事务对象START TRANSACTIONtx.begin()开始事务COMMITtx.commit()提交ROLLBACKtx.rollback()回滚SQL 语句本身一个字都不用改还是走conn.prepareStatement(...)——事务是挂在连接上的一层状态从begin()到commit()/rollback()之间这个连接上执行的所有改动都在草稿区里。标准的写法套路是try 里干活、最后 commitcatch 里 rollbackpackage cj_mysql_demo import std.database.sql.* import mariadb.cdbc; main(): Int64 { let driver DriverManager.getDriver(mariadb).getOrThrow() let ds driver.open(mariadb://127.0.0.1:3306, [ (username, root), (password, wangpeng), (database, tumo_governance) ]) let conn ds.connect() // 演示前先把两人的余额重置为 1000 let reset conn.prepareStatement(UPDATE account SET balance 1000) reset.update() reset.close() let tx conn.createTransaction() tx.begin() try { let stmt1 conn.prepareStatement(UPDATE account SET balance balance - 100 WHERE name zhangsan) stmt1.update() println(zhangsan 扣款成功) stmt1.close() let stmt2 conn.prepareStatement(UPDATE account SET balance2 balance 100 WHERE name lisi) stmt2.update() println(lisi 加款成功) stmt2.close() tx.commit() println(事务已提交) } catch (e: SqlException) { tx.rollback() println(出错了事务已回滚${e.message}) } conn.close() 0 }第一次运行第二条还是错列名balance2zhangsan 扣款成功 出错了事务已回滚Unknown column balance2 in field list, errorCode: 0程序不再崩溃——异常被 catch 接住rollback()把草稿撕掉还打印了一句人话提示。客户端查余额两人都是 1000一分钱没丢。然后把balance2改回balance再跑一次zhangsan 扣款成功 lisi 加款成功 事务已提交余额变成了 900 / 1100。和第三节的事故版对比业务代码几乎没动只是多了领事务 → begin → try 里 commit → catch 里 rollback这层外壳批量操作的成败一致性就有了保障。最后再叮嘱三个实测出来的小细节事务对象不用关闭。它身上没有close()方法——本课实测给tx调close()会直接编译报错close is not a member of interface Transaction。commit()或rollback()一执行这个事务就结束了。catch 的类型写SqlException。它在std.database.sql里我们已经import std.database.sql.*直接用e.message里就是数据库返回的错误原文。回滚之后连接还活着。rollback()只是结束当前事务连接可以继续领下一个事务、继续干活不用重连。五、常见报错速览列名写错本课贯穿始终的那位SqlException: Unknown column balance2 in field list, errorCode: 0mariadb 驱动在prepareStatement阶段就抛出。真实项目里看到Unknown column先核对列名拼写再确认连的是不是你以为的那个库、那张表。给事务对象调 closeerror: close is not a member of interface Transaction事务不是 Statement、ResultSet 那种资源对象没有close()。结束事务的唯一方式是commit()或rollback()。六、常用 API 速查事务篇代码作用let tx conn.createTransaction()从连接创建事务对象tx.begin()开始事务之后的改动进入草稿区tx.commit()提交草稿全部落库事务结束tx.rollback()回滚草稿全部撤销事务结束固定套路begin()→try { 干活; commit() } catch (e: SqlException) { rollback() }七、常见问题 FAQQ1如果忘了 commit 也忘了 rollback程序直接关掉连接改动会落库吗不会。本课实测过begin()之后执行扣款不 commit 直接conn.close()再查余额发现改动被服务器自动回滚了——没定稿的草稿连接一断就被丢弃。但千万别把这个当成偷懒的理由显式写commit()/rollback()是清清楚楚的意图靠断连自动回滚是碰运气的习惯。Q2事务里还能用占位符吗上一课的知识不会作废吧完全兼容而且是绝配。占位符解决的是值怎么安全地进 SQL事务解决的是几条 SQL 怎么捆绑成败两个维度互不干扰。事务里的占位符写法和以前一模一样let tx conn.createTransaction() tx.begin() try { let stmt conn.prepareStatement(UPDATE account SET balance balance - ? WHERE name ?) stmt.set(1, 100) stmt.set(2, zhangsan) stmt.update() stmt.close() tx.commit() println(扣款已提交) } catch (e: SqlException) { tx.rollback() println(扣款已回滚${e.message}) }扣款已提交Q3事务是不是什么 SQL 都能回滚不是DDL建表、删表这类结构操作管不住。MySQL 里执行CREATE TABLE、DROP TABLE时会触发隐式提交——它先把当前事务悄悄提交了再干活。本课在客户端实测过STARTTRANSACTION;CREATETABLEtx_test(idINT);ROLLBACK;SHOWTABLESLIKEtx_test;------------------------------------- | Tables_in_tumo_governance (tx_test) | ------------------------------------- | tx_test | -------------------------------------ROLLBACK 喊完了tx_test表照样立在那里。所以记住边界事务保护的是 INSERT / UPDATE / DELETE 这类数据操作保护不了表结构的增删。Q4一组 SELECT 查询需要包事务吗本课的场景都是多条写操作打包这也是初学者 99% 的事务用法。至于多条查询要读到一致的数据快照这种读场景的事务涉及隔离级别等进阶概念日常练习用不到本课不展开。先把打包写操作这一件事练熟。八、课后练习前 3 题必做第 4 题选做。全部在account表上操作。第 1 题必做复现事故现场把第三节的无事务版程序敲进cj_mysql_demo并运行确认输出zhangsan 扣款成功后抛出SqlException然后用客户端查余额确认 900 / 1000 的事故现场。提示重点是亲手感受每条 update() 独立提交——第一条成功了异常拦不住它。期望输出zhangsan 扣款成功 An exception has occurred: SqlException: Unknown column balance2 in field list, errorCode: 0第 2 题必做事务版转账把第四节的事务版程序敲进工程先保持错列名balance2运行一次确认回滚且余额不变再把列名改对运行一次确认提交成功、余额 900 / 1100。提示套路是begin()→try { 干活; commit() } catch (e: SqlException) { rollback() }每次运行前程序里都有重置余额的语句放心反复跑。期望输出第一次zhangsan 扣款成功 出错了事务已回滚Unknown column balance2 in field list, errorCode: 0期望输出第二次zhangsan 扣款成功 lisi 加款成功 事务已提交第 3 题必做事务批量开户用事务打包两条 INSERT给wangwu开户存 500、给zhaoliu开户存 800两人要么一起开户成功要么都不开。提示SQL 为INSERT INTO account (name, balance) VALUES (?, ?)同一个stmt每次执行前重新set两条都update()完再commit()。期望输出插入 wangwu 成功 插入 zhaoliu 成功 事务已提交----------------------- | id | name | balance | ----------------------- | 1 | zhangsan | 1000 | | 2 | lisi | 1000 | | 3 | wangwu | 500 | | 4 | zhaoliu | 800 | -----------------------做完这题记得在客户端DELETE FROM account WHERE id 2;清理保持表演数据干净。第 4 题选做验证 DDL 隐式提交在客户端依次执行START TRANSACTION;、CREATE TABLE tx_test (id INT);、ROLLBACK;、SHOW TABLES LIKE tx_test;解释为什么 ROLLBACK 之后表还在。提示对照 Q3——DDL 会触发隐式提交事务管不住表结构的增删验证完记得DROP TABLE tx_test;。期望输出------------------------------------- | Tables_in_tumo_governance (tx_test) | ------------------------------------- | tx_test | -------------------------------------下节预告现在我们的 SQL 都散落在main里连接、拼语句、set 参数、事务……业务一多就成一锅粥。下一课手写 DAO 数据访问层——把账户表的增删改查收进一个专门的类业务代码只管说我要转账不管 SQL 怎么写。这正是第一篇第 29 课仓库层思想在数据库上的回响。系列说明本系列基于 Windows 平台 CIDE 仓颉 SDK1.2.0编写。本文全部代码、运行输出与报错文案均已在 SDK 1.2.0 MySQL 5.7 环境下逐块编译运行、逐字实测如遇 SDK 版本差异导致的细节出入以你本地版本为准欢迎评论区交流。 遇到问题扫码联系作者跟着课程练习时如果在 SDK 安装、环境变量配置、编译报错或调试上卡住欢迎扫码加作者企业微信直接咨询请备注仓颉课程离线环境下图片可能加载不出来也可以在 CIDE 菜单Help ▸ 联系作者 / Contact中查看同一张二维码应用内置兜底图无需联网。 工具下载本系列全程使用的仓颉 IDE ——CIDE免费开源、社区版GitCode 仓库 / 安装包下载https://gitcode.com/wp_upala/cide打开页面后进入发行版Releases两种包任选其一安装版下载CIDE-版本-x64-Setup.exe双击安装适合日常长期使用免安装版Portable下载CIDE-版本-x64-Portable.zip解压到任意目录即用不写注册表、不留安装痕迹拷到 U 盘也能在别的电脑直接运行包内附《使用说明.txt》。适合先试用、或在受限电脑上学习本系列课程。仓颉 SDK 请前往仓颉编程语言官网下载https://cangjie-lang.cn
阅读完成 · 觉得有帮助?
咨询建站