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

【仓颉语言入门 · 第二篇 第 11 课】更新与删除:UPDATE / DELETE

上一课我们学会了 INSERT,把数据写进了 sys_login_log。但写进去的数据不会永远正确:登录 IP 换了、失败记录要清理、录错的行要删掉。这一课补齐增删改查的另一半——改(UPDATE)和删(DELETE)。只做三件事:发出第一条 UPDATE、用占位符一次改一批、学会删数据的正确姿势;顺便认识本课最重要的安全规则:UPDATE 和 DELETE 不带 WHERE,等于全表遭殃。


目录(第二篇导航)

第一篇《仓颉语言入门》共 30 课已完结(环境搭建 → 语法 → 函数 → 类型系统 → 工程化 → 并发 → 两场实战)。第二篇在更高的起点上继续,共 26 课、4 个模块:

模块 课次 内容
一、补遗篇 01~06 正则、时间日期、命令行参数、随机数、加密哈希、二进制处理
二、数据库篇 07~15 入门与连接、建表、查询、插入、更新删除、预编译防注入、事务、手写 DAO、综合小实战
三、进阶硬核篇 16~22 反射、宏基础、deriving 实战、FFI 基础、FFI 进阶、并发容器、对象池
四、实战项目篇 23~26 HTTP 基础、HTTP 进阶、多线程日志分析器、简易爬虫
  1. 正则表达式(std.regex)
  2. 时间日期处理(std.time)
  3. 命令行参数解析(std.argopt)
  4. 随机数与概率(std.random)
  5. 加密与哈希(std.crypto)
  6. 二进制数据处理(std.binary)
  7. 数据库入门:认识 std.database.sql,连上 MySQL
  8. 建表:用代码定义表结构
  9. 查询:SELECT 与结果集遍历
  10. 插入:INSERT、占位符与自增主键
  11. 更新与删除:UPDATE / DELETE(本文)
  12. 预编译与防 SQL 注入
  13. 事务:批量操作的成败一致
  14. 手写 DAO 数据访问层
  15. 数据库综合小实战
  16. 反射:在运行时打量类型
  17. 宏基础:std.ast 与 quote
  18. deriving 宏实战
  19. 与 C 互操作基础:FFI 调用 C 函数
  20. FFI 进阶:结构体、指针与编译配置
  21. 并发容器:并发安全的集合
  22. 对象池与资源复用
  23. 实战三:HTTP 服务端基础
  24. 实战四:HTTP 进阶与 JSON 接口
  25. 实战五:多线程日志分析器
  26. 实战六:简易爬虫

一、先在客户端里把 UPDATE / DELETE 试明白

老规矩:先把 SQL 在客户端里试对,再搬进代码。第 10 课的练习把 sys_login_log 折腾得差不多了,先重建它,并灌入本课专用的 5 行固定数据(后文所有演示都以这张表为底):

USE tumo_governance;
DROP TABLE IF EXISTS sys_login_log;
CREATE TABLE sys_login_log (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    login_ip VARCHAR(50),
    login_time DATETIME,
    success INT NOT NULL DEFAULT 1
);
INSERT INTO sys_login_log (user_id, login_ip, login_time, success) VALUES
(1900010622019957256, '127.0.0.1',    '2026-07-30 22:08:44', 1),
(1900010644348391560, '192.168.1.23', '2026-07-31 08:30:00', 0),
(1900010653277109512, '10.1.1.5',     '2026-08-01 09:12:36', 0),
(1900010680132446728, '192.168.1.23', '2026-08-02 14:25:10', 1),
(1900010722168889096, '172.16.0.2',   '2026-08-03 19:40:52', 0);

第一条 UPDATE:运维发现 192.168.1.23 这台出口机要下线,先把 id 为 2 的记录改到新机器 10.0.0.8。语法是 UPDATE 表名 SET 列 = 新值 WHERE 条件:

UPDATE sys_login_log SET login_ip = '10.0.0.8' WHERE id = 2;

客户端回显(这就是 UPDATE 的"成绩单"):

Query OK, 1 row affected
Rows matched: 1  Changed: 1  Warnings: 0

用 SELECT 验证一下(第 9 课的查询功底,现在派上用场):

SELECT id, user_id, login_ip FROM sys_login_log WHERE id = 2;
+----+---------------------+----------+
| id | user_id             | login_ip |
+----+---------------------+----------+
|  2 | 1900010644348391560 | 10.0.0.8 |
+----+---------------------+----------+

现在手滑一次,看看后果——故意不写 WHERE:

UPDATE sys_login_log SET login_ip = '10.0.0.1';
Query OK, 5 rows affected
Rows matched: 5  Changed: 5  Warnings: 0

再查一遍,5 行的 login_ip 全部变成了 10.0.0.1。这就是无 WHERE 的 UPDATE:MySQL 不会拦你,它老老实实把整张表都改了。

DELETE 的三个姿势一次看完(每条都是真实回显):

DELETE FROM sys_login_log WHERE id = 5;
Query OK, 1 row affected
DELETE FROM sys_login_log WHERE success = 0;
Query OK, 2 rows affected
DELETE FROM sys_login_log;
Query OK, 2 rows affected

三条下来表已经空了,SELECT COUNT(*) 确认还剩 0 行。DELETE 不带 WHERE 就是全表清空,而且没有"撤销"。

本课最重要的一句话:UPDATE 和 DELETE 必带 WHERE;写 WHERE 之前,先用同样的条件跑一遍 SELECT COUNT(*),看看会命中几行——数对了再动手。

试完把表重建一遍(第一节开头的 SQL 原样再跑一次),后面的程序需要一个干净的 5 行环境。


二、仓颉代码:第一条 UPDATE

沿用 cj_mysql_demo 工程,把 src/main.cj 改成:

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()

    let stmt = conn.prepareStatement("""
UPDATE sys_login_log SET login_ip = '10.0.0.8' WHERE id = 2
""")
    let result = stmt.update()
    println("更新完成,影响行数:${result.rowCount}")

    let stmt2 = conn.prepareStatement(
        "SELECT id, user_id, login_ip FROM sys_login_log WHERE id = 2")
    let result2 = stmt2.query()
    while (result2.next()) {
        println("id=${result2.get<Int64>(1)} uid=${result2.get<Int64>(2)} ip=${result2.get<String>(3)}")
    }
    result2.close()
    stmt2.close()

    stmt.close()
    conn.close()
    0
}

运行输出:

更新完成,影响行数:1
id=2 uid=1900010644348391560 ip=10.0.0.8

和第 8 课建表、第 10 课插入一个模子:DML 语句走 update(),拿回 UpdateResult,rowCount 是受影响的行数。改完立刻 SELECT 查回验证——这是数据库开发的基本自我修养:改之前数一遍(本节没做,下一节补上),改之后查一眼。


三、占位符与批量更新

把 UPDATE 的值换成 ? 占位符,就是第 10 课的老配方。这次玩一把批量:把所有还挂着旧 IP 192.168.1.23 的记录统一改成 10.0.0.8:

let stmt = conn.prepareStatement(
    "UPDATE sys_login_log SET login_ip = ? WHERE login_ip = ?")
stmt.set(1, "10.0.0.8")
stmt.set(2, "192.168.1.23")
let result = stmt.update()
println("批量更新,影响行数:${result.rowCount}")
stmt.close()

let stmt2 = conn.prepareStatement(
    "SELECT COUNT(*) FROM sys_login_log WHERE login_ip = '10.0.0.8'")
let result2 = stmt2.query()
if (result2.next()) {
    println("10.0.0.8 现在共 ${result2.get<Int64>(1)} 条")
}
result2.close()
stmt2.close()

运行输出:

批量更新,影响行数:1
10.0.0.8 现在共 2 条

为什么"批量"只影响了 1 行?因为第二节已经把 id 为 2 的那行改成 10.0.0.8 了,WHERE login_ip = '192.168.1.23' 现在只能命中 id 为 4 的一行——表的状态是累积的,每条 SQL 都在上一步的结果上跑。COUNT 验证:10.0.0.8 共 2 条(第 2、4 行),对得上。

三个边界情况一次看全:

// 1) 无 WHERE:全表遭殃
let stmt3 = conn.prepareStatement("UPDATE sys_login_log SET login_ip = '10.0.0.1'")
println("全表更新,影响行数:${stmt3.update().rowCount}")
stmt3.close()

// 2) WHERE 条件谁都不匹配:不报错,安静地改 0 行
let stmt4 = conn.prepareStatement(
    "UPDATE sys_login_log SET login_ip = '10.0.0.8' WHERE id = 999")
println("匹配 0 行,影响行数:${stmt4.update().rowCount}")
stmt4.close()

// 3) 新值和旧值一模一样:也不报错
let stmt5 = conn.prepareStatement("UPDATE sys_login_log SET success = 1 WHERE id = 1")
println("同值更新,影响行数:${stmt5.update().rowCount}")
stmt5.close()

运行输出:

全表更新,影响行数:5
匹配 0 行,影响行数:0
同值更新,影响行数:0

第三个例子藏着 rowCount 的真语义:id 为 1 的行 success 本来就是 1,"碰到"了 1 行,但"真的变"了 0 行——rowCount 数的是后者。客户端那一行 Rows matched: 1 Changed: 0 说的就是同一件事。记住一句话:rowCount 是"真的变了/真的删了"的行数,不是"条件命中"的行数。


四、DELETE:按 id 删、按条件删、全表删

删数据的程序写法和 UPDATE 完全同构,只是 SQL 换成 DELETE:

let stmt = conn.prepareStatement("DELETE FROM sys_login_log WHERE id = 5")
println("按 id 删除,影响行数:${stmt.update().rowCount}")
stmt.close()

let stmt2 = conn.prepareStatement("DELETE FROM sys_login_log WHERE success = 0")
println("按条件删除,影响行数:${stmt2.update().rowCount}")
stmt2.close()

let stmt3 = conn.prepareStatement("DELETE FROM sys_login_log")
println("清空删除,影响行数:${stmt3.update().rowCount}")
stmt3.close()

let stmt4 = conn.prepareStatement("DELETE FROM sys_login_log WHERE id = 999")
println("再删一次,影响行数:${stmt4.update().rowCount}")
stmt4.close()

let stmt5 = conn.prepareStatement("SELECT COUNT(*) FROM sys_login_log")
let result = stmt5.query()
if (result.next()) {
    println("表里还剩 ${result.get<Int64>(1)} 行")
}
result.close()
stmt5.close()

运行输出(接着第三节的表状态:5 行,success 依次是 1、0、0、1、0):

按 id 删除,影响行数:1
按条件删除,影响行数:2
清空删除,影响行数:2
再删一次,影响行数:0
表里还剩 0 行

逐条复盘:按 id 删走 1 行;按条件删把 success = 0 的 2 行(id 为 2、3)收走;无 WHERE 的清空把剩下的 id 为 1、4 两行送走;最后对空表再删一次,rowCount 是 0、不报错——"删了 0 行"和"改了 0 行"一样,是正常结果不是异常。要不要提示用户"没删到东西",由你的代码判断 rowCount 来决定。

动手前先数一遍:DELETE 前用同条件跑 SELECT COUNT(*),是最后一道人肉防线。程序化场景想"删错了能整体回滚",等第 13 课的事务。


五、常见报错速览

更新删除阶段的报错集中在"列名写错"和"语句残缺",提前混个脸熟:

WHERE 里列名写错:

SqlException: Unknown column 'idd' in 'where clause', errorCode: 0

SET 里列名写错:

SqlException: Unknown column 'login_ipp' in 'field list', errorCode: 0

SQL 写残了(比如少写了新值):

SqlException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 1, errorCode: 0

拿 query() 发 UPDATE(语句发不出去,数据原封不动,已实测确认):

MariaException: Execute non-select statements using the query method.

最后这条的文案有点迷惑人:它字面意思像"非 SELECT 语句请用 query 方法",其实是驱动在说"非 SELECT 语句请走 update()"。看到它就检查:改数据的语句,是不是误用了 query()。


六、常用 API 速查(更新删除篇)

任务 写法
执行 UPDATE / DELETE conn.prepareStatement(sql).update()
影响行数 UpdateResult.rowCount(数"真的变了/删了"的行)
占位符填值 stmt.set(序号, 值),序号从 1 开始
改前自查 同条件先 SELECT COUNT(*),核对行数再执行
空表/未命中 rowCount = 0,不报错
更新时间列 set(序号, DateTime.of(...)),记得 import std.time.*
DML 禁止用 query() 会抛 MariaException

七、常见问题 FAQ

Q1:rowCount 为 0,是我的语句没执行吗?
两种情况都返回 0,注意区分:WHERE 没命中任何行(Rows matched: 0),或命中了但值本来就没差(Rows matched: 1, Changed: 0)。语句本身都成功执行了——UPDATE/DELETE 不同于 SELECT,"没找到目标"不是错误。想知道"到底碰没碰到",同条件先 COUNT 一把。

Q2:怎么防止"一改全表、一删全库"?
三道防线:① 习惯上 UPDATE/DELETE 必带 WHERE,SQL 写完先扫一眼有没有 WHERE;② 动手前用同条件 SELECT COUNT(*) 核对行数,和预期对不上就停下;③ 真正的保险是第 13 课的事务——改错了能整体回滚。另外开发期可以给账号只授最小权限,但那是运维话题,本课先管住自己的 SQL。

Q3:UPDATE 想更新时间列,占位符能传 DateTime 吗?
能,和第 10 课 INSERT 的用法一致:

let stmt = conn.prepareStatement("UPDATE sys_login_log SET login_time = ? WHERE id = 1")
stmt.set(1, DateTime.of(year: 2026, month: 8, dayOfMonth: 1, hour: 12, minute: 0, second: 0))
let result = stmt.update()
println("时间列更新,影响行数:${result.rowCount}")
时间列更新,影响行数:1

DateTime 在 std.time 包里,文件顶部要有 import std.time.*。

Q4:DELETE 了还能恢复吗?
不能。DELETE 是物理删除,没有"回收站"。重要数据要么靠 WHERE 精确命中(先 COUNT),要么靠事务回滚(第 13 课),要么在业务层做"软删除"——加一列标记,"删除"只是把标记改成 1。第 15 课综合实战会用到软删除的思路。


八、课后练习

前 3 题必做,第 4 题选做。先跑一遍第一节开头的重建 SQL,保证表是干净的 5 行种子数据。

第 1 题(必做)占位符改单行

在 cj_mysql_demo 工程里,用占位符把 id 为 3 的记录的 login_ip 改成 "10.0.0.8",打印 rowCount,再 SELECT 查回这行验证。

提示:SQL 为 UPDATE sys_login_log SET login_ip = ? WHERE id = ?,两个 ? 分别 set(1, "10.0.0.8")、set(2, 3);查回的写法照搬第二节。

期望输出:

更新完成,影响行数:1
id=3 uid=1900010653277109512 ip=10.0.0.8

第 2 题(必做)批量修正失败记录

把所有 success = 0 的失败记录统一改成 success = 1(假设这些用户重新登录成功了),打印 rowCount,再用 SELECT COUNT(*) 统计 success = 0 的剩余行数。

提示:SQL 为 UPDATE sys_login_log SET success = ? WHERE success = ?;种子数据里 success = 0 的有 3 行。

期望输出:

批量更新,影响行数:3
剩余失败记录:0 条

第 3 题(必做)亲手体验一次"全表遭殃",再恢复现场

故意执行一条不带 WHERE 的 UPDATE sys_login_log SET login_ip = '10.0.0.1',观察 rowCount;然后把第一节的重建 SQL 再跑一遍,把现场恢复回来。

提示:这道题的目的就是让你在练习环境里安全地踩一次坑——真实项目里,这条 SQL 可能是事故通报的起点。

期望输出:

全表更新,影响行数:5

第 4 题(选做)给日志"瘦身"

只保留最近 3 条日志(id 最大的 3 条),把其余的删掉,打印删除的行数,并 SELECT COUNT(*) 验证剩 3 行。

提示:两步走——先用 SELECT id FROM sys_login_log ORDER BY id DESC LIMIT 1 OFFSET 2(LIMIT 负责取几行,OFFSET 负责先跳过几行——这里跳过最新的 2 条,取出第 3 新的 id)找出"分界线",再 DELETE FROM sys_login_log WHERE id < 分界id;也可以先 SELECT COUNT(*) 拿总数,DELETE ... WHERE id <= 总数 - 3。

期望输出(id 从 1 连续编起时):

瘦身完成,删除 2 行
表里还剩 3 行

下节预告

到这里,增删改查四件套凑齐了三件半,但代码里还埋着一颗雷:SQL 里的值是拼出来的。用户名输入一个 ' OR '1'='1,你的查询就查出了全表——这就是 SQL 注入,Web 安全榜单的常客。下一课 预编译与防 SQL 注入——亲眼看看一次完整的注入过程,再用占位符把它防得明明白白。


系列说明:本系列基于 Windows 平台 + CIDE + 仓颉 SDK(1.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
posted @ 2026-10-21 00:11  upala  阅读(7)  评论(0)    收藏  举报