【仓颉语言入门 · 第二篇 第 8 课】建表用代码定义表结构上一课我们连上了tumo_governance库但程序除了握手什么也没干——因为还不知道怎么让数据库干活。数据库里的活分两大类DDLData Definition Language定义表结构和DMLData Manipulation Language增删改查数据。这一课先学 DDL 里最核心的一件用仓颉代码执行CREATE TABLE亲手建出一张表。建表的语法不难难的是看懂一张真实表为什么这么设计——所以我们不从玩具表讲起直接从库里真实的sys_user表开刀。目录第二篇导航第一篇《仓颉语言入门》共 30 课已完结环境搭建 → 语法 → 函数 → 类型系统 → 工程化 → 并发 → 两场实战。第二篇在更高的起点上继续共26 课、4 个模块模块课次内容一、补遗篇0106正则、时间日期、命令行参数、随机数、加密哈希、二进制处理二、数据库篇0715入门与连接、建表、查询、插入、更新删除、预编译防注入、事务、手写 DAO、综合小实战三、进阶硬核篇1622反射、宏基础、deriving 实战、FFI 基础、FFI 进阶、并发容器、对象池四、实战项目篇2326HTTP 基础、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 与 quotederiving 宏实战与 C 互操作基础FFI 调用 C 函数FFI 进阶结构体、指针与编译配置并发容器并发安全的集合对象池与资源复用实战三HTTP 服务端基础实战四HTTP 进阶与 JSON 接口实战五多线程日志分析器实战六简易爬虫一、先看看真实的表长什么样建表之前先学会读表。打开 MySQL 命令行客户端mysql -u root -p看看上一课那个库里的用户表USEtumo_governance;DESCsys_user;DESCdescribe 的缩写用来查看一张表的结构定义输出如下------------------------------------------------------ | 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列名idType数据类型bigint(20)大整数Null是否允许为空NO必须有值Key是否索引/主键PRI主键Default不写时的默认值sex默认 1Extra附加修饰空注意这里先把这张表的样子记在脑子里下面建我们自己的表时每个修饰词都能在这里找到对照。二、CREATE TABLE 逐行讲假设系统要记录谁、什么时候、从哪个 IP 登录过、成没成功——这类日志数据不该混在用户表里得单开一张sys_login_log登录日志表。建表语句如下CREATETABLEsys_login_log(idBIGINTPRIMARYKEYAUTO_INCREMENT,user_idBIGINTNOTNULL,login_ipVARCHAR(50),login_timeDATETIME,successINTNOTNULLDEFAULT1);一列一行每行都是同一个句式列名 类型 [约束…]。逐行拆解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普通整数Int32BIGINT大整数主键常用Int64VARCHAR(n)变长字符串StringDATETIME/TIMESTAMP日期时间DateTimeDECIMAL(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 客户端DESCsys_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_incrementsuccess默认值是 1。反过来表不存在时DESC会直接报错ERROR 1146 (42S02): Table tumo_governance.sys_login_log doesnt 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_logsys_user等库里已有的表一个都不许动。以后在任何真实项目里执行 DROP 前都默念三遍表名。六、常用 API 速查建表篇任务写法执行 DDLconn.prepareStatement(sql).update()建表可重复执行CREATE TABLE IF NOT EXISTS 表名 (...)删表可重复执行DROP TABLE IF EXISTS 表名看影响行数UpdateResult.rowCountDDL 恒为 0客户端看表结构DESC 表名客户端看完整建表语句SHOW CREATE TABLE 表名七、常见问题 FAQQ1建表明明成功了为什么 rowCount 是 0rowCount数的是受影响的数据行数DDL 改的是表结构不触碰数据行所以恒为 0。判断 DDL 成败的标准是没抛异常就是成功——失败时服务器会拒绝执行驱动抛出SqlException就像第四节的 already exists。Q2VARCHAR(50)的 50 是字符数还是字节数字符数。只要库是utf8mb4字符集一个中文、一个 emoji 都只算 1 个字符。这也是第 7 课坚持建库用utf8mb4的原因之一。Q3列名、表名能用中文吗语法上用反引号包住可以用户但强烈不建议不同数据库、不同工具链对中文标识符的支持参差不齐给自己挖坑。业界习惯是小写字母 下划线如sys_login_log。Q4INT和BIGINT怎么选主键、用户 ID 这类只涨不跌、可能很大的用BIGINT对应仓颉Int64年龄、状态这种小范围整数用INTInt32就够。拿不准就用BIGINT多几个字节换不越界值。八、课后练习前 3 题必做第 4 题选做。都在tumo_governance库里操作只许动自己建的练习表。第 1 题必做建一张操作日志表仿照课文在cj_mysql_demo工程里建一张sys_op_log操作日志表要求四列id自增主键、user_idBIGINT非空、op_descVARCHAR(200)操作描述、op_timeDATETIME。提示连接部分的代码与课文完全相同只换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 doesnt exist第 4 题选做读一份真实的建表语句在 MySQL 客户端执行SHOWCREATETABLEsys_user对照输出回答两个问题sys_user的id列和我们建的sys_login_log的id列差在哪个修饰词这张表的存储引擎ENGINE和字符集CHARSET分别是什么下节预告表有了可里面还是空的——sys_user里倒是躺着 9 行真实用户数据正等着被读。下一课查询SELECT 与结果集遍历——发出第一条 SELECT用next()一行行把数据搬进仓颉程序并处理数据库里的 NULL 到了仓颉怎么办。系列说明本系列基于 Windows 平台 CIDE 仓颉 SDK1.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