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

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

上一课我们连上了 tumo_governance 库,但程序除了握手什么也没干------因为还不知道怎么让数据库干活。数据库里的活分两大类:DDL (Data Definition Language,定义表结构)和 DML (Data Manipulation Language,增删改查数据)。这一课先学 DDL 里最核心的一件:用仓颉代码执行 CREATE TABLE,亲手建出一张表。建表的语法不难,难的是看懂一张真实表"为什么这么设计"------所以我们不从玩具表讲起,直接从库里真实的 sys_user 表开刀。


目录(第二篇导航)

第一篇《仓颉语言入门》共 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. 实战六:简易爬虫

一、先看看真实的表长什么样

建表之前,先学会"读表"。打开 MySQL 命令行客户端(mysql -u root -p),看看上一课那个库里的用户表:

sql 复制代码
USE tumo_governance;
DESC sys_user;

DESC(describe 的缩写)用来查看一张表的结构定义,输出如下:

text 复制代码
+-------------+--------------+------+-----+---------+-------+
| Field       | Type         | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+-------+
| id          | bigint(20)   | NO   | PRI | NULL    |       |
| tenant_id   | bigint(20)   | YES  |     | NULL    |       |
| user_name   | varchar(50)  | YES  |     | NULL    |       |
| nick_name   | varchar(50)  | YES  |     | NULL    |       |
| password    | varchar(100) | YES  |     | NULL    |       |
| sex         | int(11)      | NO   |     | 1       |       |
| dept_id     | bigint(20)   | YES  |     | NULL    |       |
| phone       | varchar(15)  | YES  |     | NULL    |       |
| email       | varchar(20)  | YES  |     | NULL    |       |
| avatar      | varchar(200) | YES  |     | NULL    |       |
| login_time  | timestamp    | YES  |     | NULL    |       |
| user_status | int(11)      | NO   |     | 1       |       |
| create_time | timestamp    | YES  |     | NULL    |       |
| create_by   | varchar(32)  | YES  |     | NULL    |       |
| update_time | timestamp    | YES  |     | NULL    |       |
| update_by   | varchar(32)  | YES  |     | NULL    |       |
| del_flag    | int(11)      | NO   |     | 1       |       |
+-------------+--------------+------+-----+---------+-------+

一张表的结构就是若干列的定义,每列六个属性:

列 含义 读法示例(id 列)
Field 列名 id
Type 数据类型 bigint(20) 大整数
Null 是否允许为空 NO 必须有值
Key 是否索引/主键 PRI 主键
Default 不写时的默认值 sex 默认 1
Extra 附加修饰 空,注意这里

先把这张表的样子记在脑子里,下面建我们自己的表时,每个修饰词都能在这里找到对照。


二、CREATE TABLE 逐行讲

假设系统要记录"谁、什么时候、从哪个 IP 登录过、成没成功"------这类日志数据不该混在用户表里,得单开一张 sys_login_log 登录日志表。建表语句如下:

sql 复制代码
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
);

一列一行,每行都是同一个句式:列名 类型 约束...。逐行拆解:

  • id BIGINT PRIMARY KEY AUTO_INCREMENT:大整数主键,AUTO_INCREMENT 表示插入时不用给值,数据库自动编号 (1、2、3......)。回头看 sys_user 的 id 列------它的 Extra 是空的,没有自增,说明它的编号是程序自己算好再写入的(真实项目常用分布式 ID)。两种方式都合法,我们练习表选自增,最省心。
  • user_id BIGINT NOT NULL:这条日志属于哪个用户,NOT NULL 表示不允许空着------没主人的日志没意义。
  • login_ip VARCHAR(50):变长字符串,最长 50 个字符。不写 NOT NULL 就是默认可空。
  • login_time DATETIME:日期时间类型(YYYY-MM-DD HH:MM:SS)。sys_user 里用的是 TIMESTAMP,两者都能存时间,新手阶段知道都是"时间"即可。
  • success INT NOT NULL DEFAULT 1:是否登录成功,1 成 0 败,DEFAULT 1 表示插入时没写这一列就自动填 1。

常用 MySQL 类型先认这几个,括号里是对应的仓颉类型(下一课查询取数时真正用到,现在混个眼熟):

MySQL 类型 用途 仓颉类型
INT 普通整数 Int32
BIGINT 大整数(主键常用) Int64
VARCHAR(n) 变长字符串 String
DATETIME / TIMESTAMP 日期时间 DateTime
DECIMAL(m,n) 精确小数(金额) Decimal

三、仓颉代码:把建表语句发出去

沿用上节课的 cj_mysql_demo 工程(驱动、依赖都不动),把 src/main.cj 改成:

cangjie 复制代码
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()

    // 建表 SQL:定义登录日志表
    let sql = """
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
)
"""
    let stmt = conn.prepareStatement(sql)
    let result = stmt.update()
    println("建表完成,影响行数:${result.rowCount}")

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

运行输出:

text 复制代码
建表完成,影响行数:0

四个要点:

  • DDL 走 update(),不走 query() :query() 期待服务器返回一张结果表(SELECT 才有);建表没有结果表,只有"执行完毕"的确认,所以走 update() 通道。
  • 影响行数:0 不是失败 :update() 返回的 UpdateResult 里有个 rowCount 属性,表示这条 SQL 影响了多少数据行 。建表动的是结构不是数据,恒为 0。(UpdateResult 还有个 lastInsertId 属性,那是第 10 课插入自增主键时才登场的角色。)
  • SQL 用三引号原样写 :仓颉的 """ 多行字符串让 SQL 保持原本的换行排版,不用拼串,可读性最好。
  • Statement 也要 close:和连接一样,用完还回去。

小提示 :编译时会看到 warning: unused import 'mariadb.cdbc' 警告------这是正常的。这行 import 的价值在于触发驱动自注册(第 7 课 FAQ 讲过),编译器看不到包里的名字被直接使用才提醒,忽略即可。


四、再跑一次:表已存在与 IF NOT EXISTS

别改代码,把上面的程序原样再跑一次,这次程序直接抛异常:

text 复制代码
An exception has occurred:
SqlException: Table 'sys_login_log' already exists, errorCode: 0

(下面还会跟着一串调用栈,指明出错位置。)

服务器的态度很明确:同名表已经存在,拒绝再建。这在"程序每次启动都要确保表存在"的场景下很烦------总不能先人肉查一遍。SQL 早就备好了台阶:CREATE TABLE IF NOT EXISTS,存在就跳过、不存在才创建:

cangjie 复制代码
let stmt = conn.prepareStatement("""
CREATE TABLE IF NOT EXISTS 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
)
""")
let result = stmt.update()
println("建表完成,影响行数:${result.rowCount}")
stmt.close()

改完再跑(无论跑几遍),输出都恢复正常:

text 复制代码
建表完成,影响行数:0

记住这个搭配:能重复执行的建表语句都该带上 IF NOT EXISTS。


五、验证成果,然后收拾干净

5.1 用客户端确认表真的建出来了

代码说成功不算数,亲眼看看。回到 MySQL 客户端:

sql 复制代码
DESC sys_login_log;
text 复制代码
+------------+-------------+------+-----+---------+----------------+
| Field      | Type        | Null | Key | Default | Extra          |
+------------+-------------+------+-----+---------+----------------+
| id         | bigint(20)  | NO   | PRI | NULL    | auto_increment |
| user_id    | bigint(20)  | NO   |     | NULL    |                |
| login_ip   | varchar(50) | YES  |     | NULL    |                |
| login_time | datetime    | YES  |     | NULL    |                |
| success    | int(11)     | NO   |     | 1       |                |
+------------+-------------+------+-----+---------+----------------+

和我们写的定义一字不差:id 带上了 auto_increment,success 默认值是 1。反过来,表不存在时 DESC 会直接报错 ERROR 1146 (42S02): Table 'tumo_governance.sys_login_log' doesn't exist------所以能打出结构,就是建成功了。

5.2 DROP TABLE:删表只需一句

练习表不想留了,删掉:

cangjie 复制代码
let stmt = conn.prepareStatement("DROP TABLE IF EXISTS sys_login_log")
let result = stmt.update()
println("删表完成,影响行数:${result.rowCount}")
stmt.close()
text 复制代码
删表完成,影响行数:0

和建表镜像:DROP TABLE 在表不存在时会报错,加上 IF EXISTS 就可以放心重复执行。

⚠️ 红线 :DROP TABLE 删的是整张表连同里面的所有数据 ,没有回收站、没有确认弹窗,执行即消失。本课只允许删你自己刚建的练习表(sys_login_log),sys_user 等库里已有的表一个都不许动。以后在任何真实项目里,执行 DROP 前都默念三遍表名。


六、常用 API 速查(建表篇)

任务 写法
执行 DDL conn.prepareStatement(sql).update()
建表(可重复执行) CREATE TABLE IF NOT EXISTS 表名 (...)
删表(可重复执行) DROP TABLE IF EXISTS 表名
看影响行数 UpdateResult.rowCount(DDL 恒为 0)
客户端看表结构 DESC 表名
客户端看完整建表语句 SHOW CREATE TABLE 表名

七、常见问题 FAQ

Q1:建表明明成功了,为什么 rowCount 是 0? rowCount 数的是"受影响的数据行数",DDL 改的是表结构,不触碰数据行,所以恒为 0。判断 DDL 成败的标准是:没抛异常就是成功 ------失败时服务器会拒绝执行,驱动抛出 SqlException(就像第四节的 already exists)。

Q2:VARCHAR(50) 的 50 是字符数还是字节数? 字符数。只要库是 utf8mb4 字符集,一个中文、一个 emoji 都只算 1 个字符。这也是第 7 课坚持建库用 utf8mb4 的原因之一。

Q3:列名、表名能用中文吗? 语法上用反引号包住可以(用户),但强烈不建议:不同数据库、不同工具链对中文标识符的支持参差不齐,给自己挖坑。业界习惯是小写字母 + 下划线 ,如 sys_login_log。

Q4:INT 和 BIGINT 怎么选? 主键、用户 ID 这类"只涨不跌、可能很大"的用 BIGINT(对应仓颉 Int64);年龄、状态这种小范围整数用 INT(Int32)就够。拿不准就用 BIGINT,多几个字节换不越界,值。


八、课后练习

前 3 题必做,第 4 题选做。都在 tumo_governance 库里操作,只许动自己建的练习表。

第 1 题(必做)建一张操作日志表

仿照课文,在 cj_mysql_demo 工程里建一张 sys_op_log 操作日志表,要求四列:id(自增主键)、user_id(BIGINT,非空)、op_desc(VARCHAR(200),操作描述)、op_time(DATETIME)。

提示:连接部分的代码与课文完全相同,只换 sql 变量的内容;执行用 stmt.update(),打印 result.rowCount。

期望输出:

text 复制代码
建表完成,影响行数:0

第 2 题(必做)亲手触发一次 already exists

不改代码,把第 1 题的程序再跑一次,逐字读一遍报错(应该看到 SqlException: Table 'sys_op_log' already exists);然后给建表语句加上 IF NOT EXISTS,再跑一次,确认不再报错。

这道题的目的:already exists 是建表最高频的报错,提前见过、会修,以后就永远难不倒你。

第 3 题(必做)删掉它,并确认它真的没了

用 DROP TABLE IF EXISTS sys_op_log 删掉第 1 题建的表,打印影响行数;然后到 MySQL 客户端执行 DESC sys_op_log;,确认客户端报:

text 复制代码
ERROR 1146 (42S02): Table 'tumo_governance.sys_op_log' doesn't exist

第 4 题(选做)读一份真实的建表语句

在 MySQL 客户端执行:

sql 复制代码
SHOW CREATE TABLE sys_user

对照输出回答两个问题:sys_user 的 id 列和我们建的 sys_login_log 的 id 列差在哪个修饰词?这张表的存储引擎(ENGINE)和字符集(CHARSET)分别是什么?

下节预告

表有了,可里面还是空的------sys_user 里倒是躺着 9 行真实用户数据,正等着被读。下一课 查询:SELECT 与结果集遍历 ------发出第一条 SELECT,用 next() 一行行把数据搬进仓颉程序,并处理"数据库里的 NULL 到了仓颉怎么办"。


系列说明:本系列基于 Windows 平台 + CIDE + 仓颉 SDK(1.2.0)编写。本文全部代码、运行输出与报错文案均已在 SDK 1.2.0 + MySQL 5.7 环境下逐块编译运行、逐字实测;如遇 SDK 版本差异导致的细节出入,以你本地版本为准,欢迎评论区交流。


💬 遇到问题?扫码联系作者

跟着课程练习时,如果在 SDK 安装、环境变量配置、编译报错或调试上卡住,欢迎扫码加作者企业微信 直接咨询(请备注"仓颉课程"):

离线环境下图片可能加载不出来,也可以在 CIDE 菜单 Help ▸ 联系作者 / Contact 中查看同一张二维码(应用内置兜底图,无需联网)。


📥 工具下载

本系列全程使用的仓颉 IDE ------ CIDE(免费开源、社区版):

  • GitCode 仓库 / 安装包下载 :gitcode.com/wp_upala/ci...
  • 打开页面后进入 发行版(Releases) ,两种包任选其一:
    • 安装版 :下载 CIDE-<版本>-x64-Setup.exe,双击安装,适合日常长期使用;
    • 免安装版(Portable) :下载 CIDE-<版本>-x64-Portable.zip,解压到任意目录即用,不写注册表、不留安装痕迹,拷到 U 盘也能在别的电脑直接运行(包内附《使用说明.txt》)。适合先试用、或在受限电脑上学习本系列课程。
  • 仓颉 SDK 请前往仓颉编程语言官网下载:cangjie-lang.cn
相关推荐
预知同行1 小时前
深入解析 MoE 混合专家模型:从 Router 到 All-to-All 的架构全景与生产实践
后端·架构
weiwin1231 小时前
MAF 入门(8):多模态实战——让 Agent 看懂图片、处理混合内容
人工智能·后端
弈栈录2 小时前
流式对话接口设计:SSE、WebSocket 与断线重连
后端
Java内核笔记2 小时前
Spring AI 生产级 Chat 接口:6 个开关、13 个坑,其中 2 个开关根本没生效
java·人工智能·后端
是江迪呀2 小时前
向全世界售卖你的产品:做了一款教程引导插件
前端·javascript·后端
WeiXin_DZbishe2 小时前
基于springboot装修风格选择系统的设计与开发-计算机毕设【课程设计】77964
java·vue.js·spring boot·后端·python·php·课程设计
leikooo2 小时前
ARTS 1018: 字符计数判断异位词、局部做对仍会增加维护负担与用 AI 也要理解自己在做什么
后端
一步一脚印1562 小时前
对于JVM的简单理解 一
后端
136096757233 小时前
证书签不下来,是因为 well-known 被反代了
后端