【仓颉语言入门 · 第二篇 第 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 进阶、多线程日志分析器、简易爬虫 |
- 正则表达式(std.regex)
- 时间日期处理(std.time)
- 命令行参数解析(std.argopt)
- 随机数与概率(std.random)
- 加密与哈希(std.crypto)
- 二进制数据处理(std.binary)
- 数据库入门:认识 std.database.sql,连上 MySQL
- 建表:用代码定义表结构(本文)
- 查询:SELECT 与结果集遍历
- 插入:INSERT、占位符与自增主键
- 更新与删除:UPDATE / DELETE
- 预编译与防 SQL 注入
- 事务:批量操作的成败一致
- 手写 DAO 数据访问层
- 数据库综合小实战
- 反射:在运行时打量类型
- 宏基础:std.ast 与 quote
- deriving 宏实战
- 与 C 互操作基础:FFI 调用 C 函数
- FFI 进阶:结构体、指针与编译配置
- 并发容器:并发安全的集合
- 对象池与资源复用
- 实战三:HTTP 服务端基础
- 实战四:HTTP 进阶与 JSON 接口
- 实战五:多线程日志分析器
- 实战六:简易爬虫
一、先看看真实的表长什么样
建表之前,先学会"读表"。打开 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