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

0 阅读8分钟

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

解析 BMP 文件头时,我们自己数偏移、自己定字段边界——那种"数据结构由程序自己扛"的方式处理单个文件还行,可一旦数据变成一个班的花名册,文件就捉襟见肘了:查一个学生要全文读一遍,两个程序同时写还可能互相覆盖。从这一课起我们进入数据库篇。第一步不急着写增删改——先把地基打好:认识仓颉的数据库接口 std.database.sql,配好 MySQL 环境,让程序真正"连上"数据库。能连上,后面的事就好办了。

⚠️ 本课实测状态说明:本文 API 签名均核对自仓颉 SDK 1.2.0 产物与驱动源码,代码可用度高;但撰写本文时,作者电脑因 Windows 更新(msvcrt.dll 兼容性)导致仓颉运行时暂时启动崩溃,所有 text 块均为依据接口语义与驱动官方测试用例推算的预期输出,尚未逐字实测。环境恢复后会第一时间逐块编译运行、逐字校验并移除本说明。跟练时如遇出入,请以你本地实际输出为准并反馈作者。


目录(第二篇导航)

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

一、为什么要进数据库

回顾第一篇的文件小工具:数据存成 JSON 行,每次启动全读进 ArrayList,改完再整文件写回。数据量小的时候简单美好,学生一多痛点立刻冒头:

痛点文件方案数据库方案
查一条全文读到内存,逐行比对SELECT ... WHERE id = ?,服务器直接返回
插一条读全文 → 加一行 → 整文件写回INSERT,一条语句搞定
并发写两个程序同时写,后写覆盖先写服务器统一调度,不打架

程序只负责发 SQL 语句,存储、索引、并发这些脏活全交给 MySQL 服务器(一个独立运行的后台服务):

┌──────────────┐      SQL(TCP 3306)      ┌──────────────┐
│  你的仓颉程序  │  ──────────────────────→ │  MySQL 服务器 │
│              │  ←──────────────────────  │ (存数据、执行)│
└──────────────┘      结果/影响行数          └──────────────┘

还有一个容易误会的点要先说清:标准库 std.database.sql 里没有任何能连数据库的实现,它只定义接口,就像插座标准——规定插座长什么样,但不发电:

public interface Driver {
    func open(url: String, opt: Array<(String, String)>): Datasource
}
public interface Datasource {
    func connect(): Connection
}
public interface Connection {
    func prepareStatement(sql: String): Statement
    prop state: ConnectionState
}
public interface Statement {
    func set<T>(index: Int64, value: T): Unit
    func query(): QueryResult          // 执行 SELECT
    func update(): UpdateResult        // 执行 INSERT/UPDATE/DELETE/建表
}

发电的是驱动——实现了这套接口的第三方包。本课用仓颉官方适配组(Cangjie-SIG)维护的 mariadb-driver:纯仓颉实现,同时兼容 MySQL / MariaDB / TiDB。包名虽叫 mariadb,连本机 MySQL 毫无压力(MariaDB 本就是 MySQL 的同源分支,协议一致)。

本课的目标就一个:让下面的程序打印出 Connected。建表、查询、增删改,留给后面几课慢慢来。


二、环境准备

一次性配好三样东西,后面数据库篇所有课都用这套环境。

2.1 装好 MySQL 并建库

已装过 MySQL 的同学(服务列表里能看到 MySQL57/MySQL80 且状态 Running)跳过安装。没装的到官网 dev.mysql.com/downloads/i… 下载 Installer,只勾 MySQL Server 一路 Next,记住给 root 设置的密码(本文示例统一写 123456,你要换成自己的)。

然后打开 MySQL 命令行客户端(PowerShell 执行 mysql -u root -p,输入密码),建课程专用库:

CREATE DATABASE cj_demo DEFAULT CHARACTER SET utf8mb4;

坑:字符集写 utf8mb4,别写 utf8——MySQL 里的 utf8 是残废版(最多 3 字节),存生僻字会报错。

2.2 装驱动的两个前置依赖

mariadb-driver 编译时还需要两样东西:

  1. OpenSSL 3:登录 MySQL 时密码哈希要用。装 Win32OpenSSL(slproweb.com/products/Wi…,Win64 版)。第 5 课装过的同学跳过。
  2. stdx 扩展库:驱动解析连接 URL 用到 stdx.encoding.url。到仓颉官网 cangjie-lang.cn 下载与 SDK 版本一致的 cangjie-stdx-windows-x64-xxx,解压到固定位置(作者机器上是 D:\software\cangjie\cangjie_stdx_libs\stdx),并设置系统环境变量 CANGJIE_STDX_PATH 指向这个目录。

小提示:之后 cjpm update 报找不到 stdx 包,九成是这个环境变量没设或路径指错——注意指向里面那层 stdx 目录。

2.3 建工程,引入驱动

cjpm init 一个可执行工程 cj_mysql_demo,把 cjpm.toml 改成:

[package]
  cjc-version = "1.2.0"
  name = "cj_mysql_demo"
  version = "1.0.0"
  output-type = "executable"

[dependencies]
  mariadb = {git = "https://gitcode.com/Cangjie-SIG/mariadb-driver.git", branch = "master"}

[target.x86_64-w64-mingw32]
    [target.x86_64-w64-mingw32.bin-dependencies]
        path-option = ["D:\\software\\cangjie\\cangjie_stdx_libs\\stdx"]

两处按你机器改:cjc-version 与本地 SDK 一致;path-option 改成你的 stdx 路径(双反斜杠 \\)。然后在工程根目录执行:

cjpm update

第一次需要联网拉驱动源码并编译,看到 sync success 之类提示就成功了。

2.4 第一个数据库程序

src/main.cj:

package cj_mysql_demo

import std.database.sql.*
import mariadb.cdbc;

main(): Int64 {
    // 1. 按名字取出驱动
    let driver = DriverManager.getDriver("mariadb").getOrThrow()

    // 2. 连接 URL + 连接选项,打开数据源
    let ds = driver.open("mariadb://127.0.0.1:3306", [
        ("username", "root"),
        ("password", "wangpeng"),
        ("database", "tumo_governance")
    ])

    // 3. 借一条连接,看状态,还回去
    let conn = ds.connect()
    println("连接状态:${conn.isClosed()}")
    conn.close()
    0
}
连接状态:false

看到 false,仓颉程序就和 MySQL 握上手了。报错的话先翻文末 FAQ。


三、五个对象,一条调用链

回头看 2.4 的代码,一次数据库访问就是五个角色接力:

DriverManager ──getDriver("mariadb")──▶ Driver
                                          │ open(url, 选项)
                                          ▼
                                      Datasource(数据源)
                                          │ connect()
                                          ▼
                                      Connection(连接)
                                          │ prepareStatement(sql)
                                          ▼
                                      Statement ──query()/update()──▶ 结果
对象职责类比
DriverManager驱动注册表,按名字查驱动快递客服总台
Driver一个数据库品牌的驱动某家快递公司
Datasource数据源当地分拣站
Connection一条网络连接一辆快递车
Statement一条可执行的 SQL一个要寄的包裹

三个要点:

  • 驱动是"凭空"来的:我们没写 MariaDBDriver(),是 import mariadb.cdbc.* 加载包时,驱动源码里的一行包级代码自动把自己注册进了 DriverManager。删掉这行 import,getDriver("mariadb") 就返回 None——这就是为什么必须 import 驱动包。
  • URL 和选项分工:URL 只写地址 mariadb://主机:端口(协议头必须 mariadb://);用户名、密码、库名放第二参数的元组数组里,每项是 (选项名, 值)。本课用到 username/password/database/ssl.mode 四个,最后一个 disabled 是本机关闭加密(正式环境再谈)。
  • connect 是借,close 是还:Datasource 内部有连接池,conn.close() 不是断网,是把连接还回池子。所以用完必须 close,只借不还池子会干涸。

连接对象的状态属性 state 来自枚举 ConnectionState:Connecting(连接中)、Connected(正常)、Closed(已关闭)、Broken(故障)。排障时打印它最直观。


四、常用 API 速查(连接篇)

环节API说明
查注册DriverManager.drivers()返回已注册驱动名数组
取驱动DriverManager.getDriver("mariadb").getOrThrow()import 驱动包后按注册名取
开数据源driver.open("mariadb://host:3306", [(k, v), ...])选项:username/password/database/ssl.mode
借连接ds.connect()从连接池借,不是新建 TCP
还连接conn.close()还回池子,用完必须还
看状态conn.stateConnecting / Connected / Closed / Broken

五、常见问题 FAQ

Q1:标准库为什么不自带 MySQL 驱动?包名叫 mariadb 能连 MySQL 吗? std.database.sql 只定义接口("插座标准"),实现交给生态("电器"),这是仓颉官方的分工设计。mariadb-driver 是仓颉官方适配组 Cangjie-SIG 维护的:MariaDB 本就是 MySQL 原作者分叉的同源项目,两者网络协议兼容,TiDB、OceanBase 也兼容——所以这个驱动能连一整族数据库,连谁只取决于 URL 里的主机和端口。

Q2:为什么必须 import mariadb.cdbc.*?代码里根本没用到包里的名字。 import 不只是为了"用名字",还会触发驱动的自注册(见第三节第一个要点)。不 import,DriverManager 里就没有 mariadb 这个驱动,getDriver 返回 None,getOrThrow() 当场抛异常。

Q3:连接报错怎么排查? 按出现频率排:① MySQL 服务没启动;② 密码写错或没改课文里的 123456;③ 端口不是 3306;④ CANGJIE_STDX_PATH 没配导致驱动编译不过;⑤ OpenSSL 的 DLL 不在 PATH。排障时先把 conn.isClosed() 打出来看。


六、课后练习

前 3 题必做,第 4 题选做。本课练习都围绕"连接"这一件事,不需要写任何 SQL。

第 1 题(必做)查驱动注册表

在 2.4 程序的基础上,把 println 那行改成打印驱动注册表的内容,运行确认 mariadb 驱动确实被自动注册了。

提示:用第三节提到的 DriverManager.drivers(),直接字符串插值打印即可。

main(): Int64 {
    println("已注册的驱动:${DriverManager.drivers()}")
    0
}

期望输出:

已注册的驱动:[mariadb]

第 2 题(必做)亲眼看一次连接失败

把程序里的密码故意改错(比如 wrong-password),运行观察报错;看完把密码改回来,确认恢复正常。

这道题的目的:以后你写数据库程序时,第一次报错大概率就是密码或端口不对——提前见过报错的样子,就不慌了。

第 3 题(必做)借两条连接

在 2.4 程序的基础上,连续 connect() 两次,分别打印两条连接的状态,最后把两条都 close()。

提示:两条连接分别用 conn1、conn2 两个变量接住;打印两行状态即可。

let conn1 = ds.connect()
let conn2 = ds.connect()
println("连接 1:${conn1.isClosed()}")
println("连接 2:${conn2.isClosed()}")
conn1.close()
conn2.close()

期望输出:

连接 1:false
连接 2:false

下节预告

连接跑通了,但库里只有一个空的 cj_demo,连表都没有。下一课 建表:用代码定义表结构——认识 SQL 的 DDL,用仓颉代码把 students 表建出来,为查询和增删改备好"容器"。


系列说明:本系列基于 Windows 平台 + CIDE + 仓颉 SDK(1.2.0)编写。本文 API 均核对自 SDK 与驱动源码,运行输出待环境恢复后逐字实测更新;如遇 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