news 2026/10/20 6:41:10

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【仓颉语言入门 · 第二篇 第 9 课】

【仓颉语言入门 · 第二篇 第 9 课】查询:SELECT 与结果集遍历

上一课我们建表又删表,折腾的都是结构;而sys_user里躺着的 9 行真实用户数据,一直等着被读。增删改查里,查是最高频的一个,也是唯一"有去有回"的操作——发一句 SELECT 出去,服务器会回一张结果表回来。这一课就学会三件事:发出第一条 SELECT、用next()把数据一行行搬进仓颉程序、处理好"数据库里的 NULL 到了仓颉怎么办"。


目录(第二篇导航)

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

模块课次内容
一、补遗篇01~06正则、时间日期、命令行参数、随机数、加密哈希、二进制处理
二、数据库篇07~15入门与连接、建表、查询、插入、更新删除、预编译防注入、事务、手写 DAO、综合小实战
三、进阶硬核篇16~22反射、宏基础、deriving 实战、FFI 基础、FFI 进阶、并发容器、对象池
四、实战项目篇23~26HTTP 基础、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. 实战六:简易爬虫

一、先在客户端里把 SQL 查明白

数据库开发有个好习惯:先把 SQL 在客户端里试对,再搬进代码。打开 MySQL 命令行客户端,三条查询快速过一遍。

第一条,查出所有用户的登录名和昵称——SELECT后面写列名,FROM后面写表名:

USEtumo_governance;SELECTuser_name,nick_nameFROMsys_user;
+-----------+--------------+ | user_name | nick_name | +-----------+--------------+ | admin | 管理员 | | zhangsan | 张三 | | test | 测试用户 | | lisi | 里斯 | | wangwu | 王五 | | sunqi | 孙七 | | liang | 李昂 | | hbwh | 湖北武汉 | | zhangsan1 | 张三 | +-----------+--------------+

第二条,加WHERE只看男性用户(sex = 1)——9 行变 8 行,test被过滤掉了:

SELECTuser_name,sexFROMsys_userWHEREsex=1;
+-----------+-----+ | user_name | sex | +-----------+-----+ | admin | 1 | | zhangsan | 1 | | lisi | 1 | | wangwu | 1 | | sunqi | 1 | | liang | 1 | | hbwh | 1 | | zhangsan1 | 1 | +-----------+-----+

第三条,ORDER BY ... DESC倒序排 +LIMIT 3取前三名,看看"最近登录排行榜"的榜头:

SELECTuser_name,login_timeFROMsys_userWHERElogin_timeISNOTNULLORDERBYlogin_timeDESCLIMIT3;
+-----------+---------------------+ | user_name | login_time | +-----------+---------------------+ | hbwh | 2026-07-30 22:08:44 | | zhangsan1 | 2026-07-30 21:59:18 | | admin | 2026-07-30 21:36:59 | +-----------+---------------------+

注意login_time允许为空,没登录过的用户这一列是NULL,所以用WHERE login_time IS NOT NULL先把他们请出排行榜(判断"是不是 NULL"要用IS/IS NOT,不能写= NULL)。

SELECT 的语法当然远不止这些,但本课只需要这三板斧:挑列、过滤、排序限量。SQL 试对了,下面把它们原样搬进仓颉。


二、仓颉代码:第一条 SELECT

沿用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("SELECT user_name, nick_name FROM sys_user") let result = stmt.query() while (result.next()) { let name = result.get<String>(1) let nick = result.get<String>(2) println("${name}(${nick})") } result.close() stmt.close() conn.close() 0 }

运行输出:

admin(管理员) zhangsan(张三) test(测试用户) lisi(里斯) wangwu(王五) sunqi(孙七) liang(李昂) hbwh(湖北武汉) zhangsan1(张三)

和客户端里看到的一模一样,只是现在数据进了我们的程序。四个要点:

  • query()与update()的分工(第 8 课埋的伏笔):update()发给"没有结果表"的语句(DDL、INSERT 等),query()发给 SELECT——它返回一个QueryResult结果集,服务器查出来的那张表就在里面。
  • 结果集是单向游标:result刚拿到时停在第一行之前,next()让游标前进一行——有行就返回true,没行了就返回false。所以while (result.next())是标准遍历写法:循环体每执行一次,游标正好停在一行数据上;最后一行读完,next()返回false,循环自然结束。
  • get<T>(序号)按列取数:序号是 SELECT 后面列的顺序,从 1 开始(user_name是 1,nick_name是 2);尖括号里的T是"这一列在仓颉里算什么类型",下一节细讲。
  • 三个 close 一个不能少:结果集、语句、连接,用完从小到大依次还回去。

小提示:编译时的warning: unused import 'mariadb.cdbc'老规矩,忽略即可(驱动自注册,第 7 课 FAQ 讲过)。


三、按列取数:类型要对上

get<T>的T不是随便写的——MySQL 列类型和仓颉类型有对应关系,第 8 课的类型表现在派上用场:

MySQL 类型仓颉类型(get 的泛型参数)
INTInt32
BIGINTInt64
VARCHAR(n)String
DATETIME/TIMESTAMPDateTime
DECIMAL(m,n)Decimal

对照sys_user的结构:id是bigint→Int64,user_name是varchar→String,sex是int→Int32。写个按类型取数的例子(WHERE 子句直接搬第一节试好的):

let stmt = conn.prepareStatement("SELECT id, user_name, sex FROM sys_user WHERE sex = 1") let result = stmt.query() while (result.next()) { let id = result.get<Int64>(1) let name = result.get<String>(2) let sex = result.get<Int32>(3) println("id=${id} 姓名=${name} 性别=${sex}") } result.close() stmt.close()
id=1900010622019957256 姓名=admin 性别=1 id=1900010644348391560 姓名=zhangsan 性别=1 id=1900010653277109512 姓名=lisi 性别=1 id=1900010680132446728 姓名=wangwu 性别=1 id=1900010722168889096 姓名=sunqi 性别=1 id=1900010821105162120 姓名=liang 性别=1 id=1900010933547430664 姓名=hbwh 性别=1 id=1900010933650395016 姓名=zhangsan1 性别=1

19 位的分布式 ID 原样取出,Int64装得下。取错类型会怎样?两种结局:

  • get<String>最宽容:就算列是bigint,驱动也会帮你转成字符串,get<String>(1)能拿到"1900010622019957256"。拿不准时先按 String 取来看看,是排障小技巧。
  • 数值类型量级不够会溢出:id用get<Int32>(1)取,程序当场抛OverflowException: The result would be greater than Int32.Max.——19 位的数塞不进 32 位。所以主键 ID 这类大数,老老实实用Int64。

还有一个高频场景:只想要一个数字,比如用户总数。COUNT(*)的结果当作Int64取即可;聚合查询保证只返回一行,用if代替while更贴切:

let stmt = conn.prepareStatement("SELECT COUNT(*) FROM sys_user") let result = stmt.query() if (result.next()) { println("用户总数:${result.get<Int64>(1)}") } result.close() stmt.close()
用户总数:9

四、数据库的 NULL:到了仓颉就是 Option

前三节取的列都有值,现在捅一下马蜂窝。avatar(头像)列允许为空,9 个用户里 8 个是NULL——用get<String>直取看看:

let stmt = conn.prepareStatement("SELECT user_name, avatar FROM sys_user") let result = stmt.query() while (result.next()) { let name = result.get<String>(1) let avatar = result.get<String>(2) println("${name} 的头像:${avatar}") } result.close() stmt.close()
admin 的头像: An exception has occurred: NoneValueException at mariadb.cdbc::QueryResultImpl::get<...>(Int64)(...) at cj_mysql_demo::main()(...)

第一行admin打印完(他的头像存的是空字符串,所以冒号后面什么都没有,但不算错),游标走到第二行zhangsan——他的avatar是NULL,get当场抛NoneValueException。

这正是第 9 课(第一篇)Option 的现实意义:数据库的NULL在仓颉里没有对应的基本值,驱动把它映射成Option的None。取可空列要用getOrNull<T>,返回?T,再配??给个兜底值:

while (result.next()) { let name = result.get<String>(1) let avatar = result.getOrNull<String>(2) ?? "(未设置)" println("${name} 的头像:${avatar}") }
admin 的头像: zhangsan 的头像:(未设置) test 的头像:(未设置) lisi 的头像:(未设置) wangwu 的头像:(未设置) sunqi 的头像:(未设置) liang 的头像:(未设置) hbwh 的头像:(未设置) zhangsan1 的头像:(未设置)

9 行全须全尾地出来了。注意第一行暴露了一个重要事实:NULL和空字符串是两回事——admin 的头像是空字符串(get正常取出""),其余 8 人才是真正的NULL(走了??兜底)。数据库里这两者互不替代,程序里也走的是两条不同的代码路径。

经验法则一句话:DESC表结构时Null列是YES的,一律getOrNull;是NO的,才能放心get。


五、结果集的"说明书":columnInfos

QueryResult还随身带着每列的元数据,存在columnInfos属性里——每列一个ColumnInfo,能读出列名(name)、仓颉侧类型名(typeName)、是否可空(nullable)等:

let stmt = conn.prepareStatement("SELECT id, user_name, sex, login_time FROM sys_user") let result = stmt.query() for (info in result.columnInfos) { println("列 ${info.name}:类型 ${info.typeName},允许空 ${info.nullable}") } result.close() stmt.close()
列 id:类型 Int64,允许空 false 列 user_name:类型 String,允许空 true 列 sex:类型 Int32,允许空 false 列 login_time:类型 DateTime,允许空 true

这份"说明书"和第三节的对照表完全吻合。眼下它最大的用处是自查:拿不准某列该用什么类型取,先把columnInfos打出来看一眼。等第 14 课手写 DAO 做通用映射时,它会成为主力工具。


六、常见报错速览

查询阶段的报错就这几类,提前混个脸熟:

表名写错(和客户端报错同款,SqlException):

An exception has occurred: SqlException: Table 'tumo_governance.sys_user_zzz' doesn't exist, errorCode: 0

列名写错:

An exception has occurred: SqlException: Unknown column 'user_namee' in 'field list', errorCode: 0

列序号从 1 开始,写 0 会被拒:

An exception has occurred: IllegalArgumentException: The index must be greater than 0

结果集 close 之后还取数:

An exception has occurred: SqlException: The database Connection has been closed, errorCode: 0

前两类是 SQL 本身的问题(回客户端把语句试对),后两类是代码流程的问题(序号、close 顺序)。加上第四节的NoneValueException和第三节的OverflowException,查询的坑基本齐了。


七、常用 API 速查(查询篇)

任务写法
执行 SELECTconn.prepareStatement(sql).query()
前进到下一行result.next()(返回false表示读完)
取非空列result.get<T>(列序号),序号从 1 开始
取可空列result.getOrNull<T>(列序号) ?? 默认值
查列元数据result.columnInfos(name / typeName / nullable)
关闭结果集result.close()
类型对照INT→Int32、BIGINT→Int64、VARCHAR→String、DATETIME/TIMESTAMP→DateTime

八、常见问题 FAQ

Q1:SELECT *一把梭不行吗?为什么课文坚持写列名?
get靠列序号取数,而*的列序是跟着表结构走的——哪天表里加一列,你程序里所有序号集体错位,还未必报错。写明列名,顺序由你的 SELECT 说了算;再说只取需要的列,网络传输也省。客户端里随手*无所谓,进代码的 SQL 都该写列名。

Q2:next()遍历到一半能倒回去、或者再遍历一遍吗?
不能。结果集是单向游标,只进不退。想再用一遍就重新query()一次;如果数据要反复用,常见做法是遍历的同时存进ArrayList。

Q3:getOrNull取出来的时间直接打印,为什么套着一层Some(...)?
比如Some(2026-07-30T21:36:59+08:00)——这是Option默认的打印格式,提醒你"这是个可能有也可能没有的值"。业务输出里别直接打印 Option,先??兜底或isSome()分支拆开再打印(第一篇第 9 课的 Option 操作,忘了可以回去翻翻)。里面时间的T和+08:00是DateTime的标准格式(ISO 8601,带时区),想换格式用第 2 课学的DateTime格式化。

Q4:查询结果一行都没有时,程序会怎样?
什么都不发生——第一次next()就返回false,循环体一次都不执行,也不抛异常。"查到 0 行"不是错误,是正常结果,需要特殊提示的话自己在循环后判断。


九、课后练习

前 3 题必做,第 4 题选做。都在tumo_governance库里操作,只读sys_user,一个字符都不许改。

第 1 题(必做)查出男性用户的邮箱

在cj_mysql_demo工程里查询sys_user中sex = 1的用户,逐行打印登录名和邮箱。

提示:SQL 为SELECT user_name, email FROM sys_user WHERE sex = 1;两列都是varchar,用get<String>(1)、get<String>(2)取。

期望输出:

admin 的邮箱:178421@163.com zhangsan 的邮箱:178451@163.com lisi 的邮箱:145165@163.com wangwu 的邮箱:26151645@163.com sunqi 的邮箱:178465@163.com liang 的邮箱:123456@163.com hbwh 的邮箱:178421@163.com zhangsan1 的邮箱:145563.@com

第 2 题(必做)统计用户总数

用SELECT COUNT(*) FROM sys_user统计并打印用户总数。

提示:聚合查询只有一行,用if (result.next());COUNT(*)的结果用get<Int64>(1)取。

期望输出:

用户总数:9

第 3 题(必做)亲手踩一次 NoneValueException,再修好它

分两步:① 查询user_name和login_time,循环里用get<String>(2)直取登录时间并打印,运行确认程序打印出 admin、zhangsan 两行后抛NoneValueException;② 把直取改成下面的分支写法,确认 9 行全部打印。

提示:login_time是时间类型,可空列用getOrNull<DateTime>(2)取(需要import std.time.*);Option用isSome()判断有值、getOrThrow()取出。

let t = result.getOrNull<DateTime>(2) if (t.isSome()) { println("${name} 最近登录:${t.getOrThrow()}") } else { println("${name} 从未登录") }

期望输出(顺序即表中行的顺序):

admin 最近登录:2026-07-30T21:36:59+08:00 zhangsan 最近登录:2026-07-05T18:32:05+08:00 test 从未登录 lisi 从未登录 wangwu 最近登录:2026-07-30T18:00:44+08:00 sunqi 最近登录:2026-07-20T17:38:04+08:00 liang 最近登录:2026-07-20T17:39:07+08:00 hbwh 最近登录:2026-07-30T22:08:44+08:00 zhangsan1 最近登录:2026-07-30T21:59:18+08:00

第 4 题(选做)最近登录排行榜

打印"最近登录排行榜":只统计登录过的用户,按login_time从近到远排序,逐行输出名次。

提示:SQL 就用第一节客户端里试过的WHERE login_time IS NOT NULL ORDER BY login_time DESC(去掉 LIMIT 取全部);另开一个var rank = 1计数器,每行打印后rank++;login_time已被 WHERE 排除空值,可以安全get<DateTime>(2)。

期望输出:

第1名 hbwh(2026-07-30T22:08:44+08:00) 第2名 zhangsan1(2026-07-30T21:59:18+08:00) 第3名 admin(2026-07-30T21:36:59+08:00) 第4名 wangwu(2026-07-30T18:00:44+08:00) 第5名 liang(2026-07-20T17:39:07+08:00) 第6名 sunqi(2026-07-20T17:38:04+08:00) 第7名 zhangsan(2026-07-05T18:32:05+08:00)

下节预告

查了半天,sys_user还是雷打不动的 9 行——我们只敢读,不敢写。下一课插入:INSERT、占位符与自增主键——往自己建的练习表里写入第一行数据,学set()占位符参数怎么传、UpdateResult.lastInsertId怎么把数据库自动编的主键拿回来。


系列说明:本系列基于 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
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 6:41:09

ppt-master:代码驱动、模板约束的PPT生成框架

1. 为什么我又造了一个PPT工具先坦白讲&#xff0c;市面上做PPT的工具已经多到用不完了。从传统的桌面端软件&#xff0c;到各种在线协作平台&#xff0c;再到这两年冒出来的一堆AI生成PPT的服务&#xff0c;按理说轮不到我再折腾一个叫ppt-master的东西。但我在实际工作里反复…

作者头像 李华
网站建设 2026/10/20 6:40:31

11月8日:计算机先驱、LeNet-5与遥控器如何串起智能机器进化史

1. 三个看似无关的事件&#xff0c;为什么值得放在同一天讲11月8日这个日子&#xff0c;在计算机发展的时间轴上有点意思。同一天里&#xff0c;诞生了一位计算机科学的奠基者&#xff0c;公开了一个改变整个视觉识别领域的网络结构&#xff0c;还出现了一项把"无线操控&q…

作者头像 李华
网站建设 2026/10/20 6:39:52

LiteLLM:大模型API协议翻译层与多供应商统一调用实践

1. 一个被严重低估的“协议翻译官”&#xff1a;LiteLLM到底在解决什么真问题&#xff1f;很多人第一次看到litellm这个名字&#xff0c;下意识会以为是某个新出的轻量级大模型——毕竟带个“LLM”后缀&#xff0c;又长得像“LiteLLaMA”或“LiteGPT”这类模型缩写。我最初也这…

作者头像 李华
网站建设 2026/10/20 6:39:47

从模糊标题到清晰方案:以rea为例的读取模块设计与实现

1. 从“rea”这个标题说起&#xff1a;一个被低估的通用缩写第一次看到“rea”这个标题&#xff0c;很多人会愣一下——三个字母&#xff0c;没有上下文&#xff0c;没有说明&#xff0c;像是谁随手敲的键盘测试。但恰恰是这种极简的标题&#xff0c;在真实的项目协作场景里出现…

作者头像 李华
网站建设 2026/10/20 6:38:36

PPT自动化生成实战:从手工劳动到工程化批量生产

1. 项目缘起与核心定位第一次看到“ppt-master”这个标题&#xff0c;我脑子里蹦出来的不是某个具体工具&#xff0c;而是一类非常典型的真实需求&#xff1a;把做PPT这件事从“手工劳动”变成“可复用、可批量、可自动化”的工程问题。你如果做过几十页以上的汇报材料&#xf…

作者头像 李华