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

文章来源声明: 原文作者:upala; 来源站点:掘金; 原文链接:https://juejin.cn/post/7697303575266836506; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

从真实表反推设计,把建表、可重复执行与验证讲透,适合仓颉初学者完成数据库 DDL 入门并建立安全操作意识。

【仓颉语言入门 · 第二篇 第 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~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. 实战六:简易爬虫

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

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

USE tumo_governance;
DESC sys_user;

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

+-------------+--------------+------+-----+---------+-------+
| 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 登录日志表。建表语句如下:

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 改成:

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
}

运行输出:

建表完成,影响行数: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

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

An exception has occurred:
SqlException: Table 'sys_login_log' already exists, errorCode: 0

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

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

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

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

建表完成,影响行数:0

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


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

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

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

DESC sys_login_log;

+------------+-------------+------+-----+---------+----------------+
| 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:删表只需一句

练习表不想留了,删掉:

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

删表完成,影响行数: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。

期望输出:

建表完成,影响行数: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;,确认客户端报:

ERROR 1146 (42S02): Table 'tumo_governance.sys_op_log' doesn't exist

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

在 MySQL 客户端执行:

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