仓颉也能这样玩 SQLite?这个库把 C 互操作的苦全替你吃了
打开数据库一行代码,建表一行代码,查数据一个循环——没错,这就是仓颉版的「开箱即用」。
## 先说痛点:仓颉调 C 库,到底有多烦?
仓颉要调用 SQLite3,第一反应是写 FFI:
```cangjie
foreign func sqlite3_open_v2(...)
foreign func sqlite3_prepare_v2(...)
foreign func sqlite3_step(...)
```
然后你会遇到一连串"亲切问候":
- 字符串要 `LibC.mallocCString` 转成 CString,用完还得释放;
- 数据库句柄、语句句柄全是 `CPointer<sqlite3>`、`CPointer<sqlite3_stmt>`;
- 一条最简单的 INSERT,要经历 **prepare → bind → step → finalize** 四步;
- 查询结果还得自己判断列类型、手动拼字符串……
写业务逻辑的时间,全耗在跟指针搏斗上了。
## 解法:sqlite3cj —— 一个库,两层 API
**sqlite3cj** 是仓颉语言的原生 SQLite3 封装库,内置 SQLite3 引擎,**Windows / Linux 双平台开箱即用**(Windows 随库分发 `sqlite3.dll`,Linux 分发 `libsqlite3.so`),不需要装任何额外环境。
| 层 | 包名 | 适合谁 |
|---|---|---|
| **友好 API** | `sqlite3cj` | 90% 的日常开发:打开、建表、增删改查、事务 |
| **底层 FFI** | `sqlite3cj.c` | 需要直接操作 `sqlite3_stmt`、绑定 BLOB 等高级场景 |
下面从零开始,一步步教你怎么用。
## 第 0 步:准备环境
**仓颉 SDK 必须是 1.0.4**(旧版标准库 API 不兼容),检查版本:
在你的项目 `cjpm.toml` 加上依赖:
```toml
[dependencies]
sqlite3cj = { git = "https://gitee.com/longkicode/sqlite3cj.git", commitId = "<最新提交hash>" }
```
拉取并编译:
```bash
cjpm update
cjpm build
## 第 2 步:打开 / 关闭数据库
```cangjie
import sqlite3cj as sq
import std.core.*
main(): Int64 {
// 打开内存库(程序结束数据自动消失,适合测试)
let db = sq.open(":memory:")
// 打开文件库(不存在自动创建)
// let db = sq.open("data.db")
// 用完记得关(重复 close 无副作用)
db.close()
println("closed=${db.isClosed()}")
return 0
}
```
**打开失败**(比如目录不存在、文件损坏)会抛异常,用 try/catch 接:
```cangjie
try {
let db = sq.open("data.db")
} catch (e: Exception) {
println("打开失败: ${e.message}")
}
```
## 第 3 步:建表
直接写 SQL:
```cangjie
db.exec("CREATE TABLE IF NOT EXISTS users (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT, " + // 自增主键
"name TEXT NOT NULL, " +
"age INTEGER, " +
"score REAL, " +
"avatar BLOB, " + // 二进制
"remark TEXT)")
```
`IF NOT EXISTS` 保证重复执行不报错。`exec` 返回受影响行数(DDL 一般为 0)。
## 第 4 步:插入数据(参数绑定)
两种写法:
```cangjie
// 写法一:值直接写进 SQL(简单,但注意别拼用户输入)
db.exec("INSERT INTO users(name, age) VALUES('zhang', 30)")
// 写法二:? 占位符 + 参数数组(推荐,防 SQL 注入)
db.exec("INSERT INTO users(name, age, score, remark) VALUES(?, ?, ?, ?)",
["li", "25", "92.0", "hello"])
```
参数规则:
- 参数统一按**文本**传,SQLite 会按列类型自动转换:`"25"` 存进 INTEGER 列就是 `25`,`"92.0"` 存进 REAL 列就是 `92.0`;
- 需要存 NULL,在 SQL 里写 `NULL` 字面量:
```cangjie
db.exec("INSERT INTO users(name, age, avatar, remark) VALUES(?, ?, NULL, NULL)",
["wang", "40"])
```
插入后取两个常用值:
```cangjie
println("影响行数: ${db.changes()}")
println("自增 id: ${db.lastInsertRowid()}")
```
## 第 5 步:查询(取记录)
`select` 返回 `Array<Array<SqlValue>>`:外层是行,内层是列,顺序与 SELECT 列一致。
```cangjie
let rows = db.select("SELECT id, name, age, score, remark FROM users ORDER BY id")
for (row in rows) {
for (v in row) {
match (v) {
case sq.SqlValue.Text(s) => print("${s}\t")
case sq.SqlValue.Null => print("NULL\t")
}
}
println("")
}
```
输出示例:
```
1 zhang 30 88.500000 NULL
2 li 25 92.000000 hello
```
各类型列在 `SqlValue` 里的样子:
| 列类型 | 取值 |
|---|---|
| INTEGER | `SqlValue.Text("30")`(字符串化) |
| REAL | `SqlValue.Text("88.500000")` |
| TEXT | `SqlValue.Text("hello")` |
| BLOB | `SqlValue.Text("<blob 4B>")`(字节数提示) |
| NULL | `SqlValue.Null` |
带参数查询 + 取列名:
```cangjie
// 带条件
let adults = db.select("SELECT * FROM users WHERE age > ?", ["30"])
// 打印表头
var header = StringBuilder()
for (c in db.columns("SELECT * FROM users")) {
header.append(c); header.append("\t")
}
println(header.toString())
```
## 第 6 步:更新 / 删除
```cangjie
db.exec("UPDATE users SET score = score + 5 WHERE age < 30")
println("更新了 ${db.changes()} 行")
db.exec("DELETE FROM users WHERE name = ?", ["li"])
println("删除了 ${db.changes()} 行")
```
`changes()` 返回**最近一次** INSERT/UPDATE/DELETE 影响的行数,在 exec 之后立刻取。
## 第 7 步:事务
要么全成功、要么全回滚:
```cangjie
db.begin()
db.exec("INSERT INTO users(name, score) VALUES('wang', 77.5)")
db.exec("INSERT INTO users(name, score) VALUES('zhao', 81.0)")
db.commit() // 提交,两条都生效
// 出错了可以回滚
db.begin()
db.exec("DELETE FROM users WHERE score < 80")
db.rollback() // 回滚,上面这条 DELETE 白执行
```
## 第 8 步:错误处理
所有失败都抛 `Exception`,`message` 就是 SQLite 原生错误信息:
```cangjie
try {
db.exec("SELEC 1") // SQL 语法错误
} catch (e: Exception) {
println(e.message)
// 输出: prepare failed [SELEC 1]: near "SELEC": syntax error
}
try {
db.select("SELECT * FROM not_exist_table")
} catch (e: Exception) {
println(e.message)
// 输出: prepare failed [SELECT * FROM not_exist_table]: no such table: not_exist_table
}
```
也可以不抛异常时自查最近错误:`db.errmsg()`。
## 第 9 步:文件库持久化(完整示例)
写入 → 关闭 → 重开读取:
```cangjie
let fdb = sq.open("kv.db")
fdb.exec("CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT)")
fdb.exec("INSERT OR REPLACE INTO kv(k, v) VALUES(?, ?)", ["greeting", "hello sqlite3cj"])
fdb.close() // 数据已落盘
// 下次启动重新打开就能读到
let rdb = sq.open("kv.db")
for (row in rdb.select("SELECT k, v FROM kv")) {
match (row[1]) {
case sq.SqlValue.Text(v) => println("greeting = ${v}")
case sq.SqlValue.Null => println("greeting = NULL")
}
}
rdb.close()
```
## 进阶:底层 FFI(BLOB 等高级场景)
友好层覆盖不了时(如绑定二进制 BLOB、逐条 step 控制),降级用 `sqlite3cj.c`:
```cangjie
import sqlite3cj.c as ffi
import std.core.*
unsafe func insertBlob(db: CPointer<ffi.sqlite3>): Unit {
try (sql = LibC.mallocCString("INSERT INTO t(data) VALUES(?)").asResource()) {
let ppStmt = LibC.malloc<CPointer<ffi.sqlite3_stmt>>(count: 8)
let rc = ffi.sqlite3_prepare_v2_cjbindwrapper(db, sql.value, -1, ppStmt, CPointer<CString>())
if (rc == ffi.SQLITE_OK) {
let stmt = ppStmt.read()
let bytes: Array<UInt8> = [0x42, 0x49, 0x4E, 0x31] // "BIN1"
let pBlob = LibC.malloc<UInt8>(count: bytes.size)
for (i in 0..bytes.size) { (pBlob + i).write(bytes[i]) }
ffi.sqlite3_bind_blob_cjbindwrapper(stmt, 1, CPointer<Unit>(pBlob), Int32(bytes.size), ffi.SQLITE_STATIC)
ffi.sqlite3_step_cjbindwrapper(stmt)
LibC.free(pBlob)
ffi.sqlite3_finalize_cjbindwrapper(stmt)
}
LibC.free(ppStmt)
}
}
```
底层共暴露 **29 个** `sqlite3_*` FFI 函数(打开/关闭/错误、prepare/step/finalize、全部 bind_*、全部 column_*、changes、last_insert_rowid),完整清单见 README「API 参考(底层 FFI)」。
---
## 完整能力清单
- **连接管理**:内存库 / 文件库、只读/读写/自动创建标志、优雅关闭
- **SQL 执行**:DDL、DML 一把梭,返回受影响行数
- **参数绑定**:`?` 占位符 + 参数数组,防 SQL 注入;文本参数自动按列类型转换
- **查询**:一行 `select` 拿全量结果,NULL / BLOB 都有明确表达
- **事务**:begin / commit / rollback 直白三连
- **自增 ID**:`lastInsertRowid()` 随时取
- **错误处理**:全部以异常形式抛出,信息就是 SQLite 原生错误
- **底层通道**:29 个 `sqlite3_*` FFI 函数全量暴露,BLOB 等高级需求随时降级使用
- **跨平台**:Windows / Linux 同一份配置自动兼容,`.dll` + `.so` 随库分发
## 适合谁
- 仓颉新手:想快点把数据存起来,不想在 FFI 里挣扎
- 业务开发:CRUD 为主,需要简单可靠的本地存储
- 工具开发:配置文件、缓存、日志落盘,一个 SQLite 文件全搞定
- 进阶玩家:底层 FFI 全量开放,想做 BLOB、自定义 VFS 也不拦你
## 项目信息
- 语言:仓颉 Cangjie 1.0.4(`cjc` / `cjpm`)
- 平台:Windows x64 / Linux(x86_64、aarch64、loongarch64)——同一份配置自动兼容,无需平台分支
- 引擎:SQLite 3.53.4(Public Domain,随库分发)
- 开源协议:Apache-2.0
- 示例:仓库 `examples/` 内含 8 组完整调用实例,clone 下来直接跑:
**Windows:**
```bat
cd sqlite3cj/examples
set PATH=..\lib;%CANGJIE_HOME%\runtime\lib\windows_x86_64_llvm;%PATH%
cjpm run # 8 组实例一次性演示
```
**Linux:**
```bash
cd sqlite3cj/examples
export LD_LIBRARY_PATH=$PWD/../lib:$CANGJIE_HOME/runtime/lib/linux_x86_64_llvm:$LD_LIBRARY_PATH
cjpm run # 8 组实例一次性演示
```
SQLite 是被几十亿台设备使用、经过数十年验证的嵌入式数据库之王。现在,仓颉开发者也可以零成本享受它。
**开箱即用,就是这么简单。**
更多推荐




所有评论(0)