打开数据库一行代码,建表一行代码,查数据一个循环——没错,这就是仓颉版的「开箱即用」。

## 先说痛点:仓颉调 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 是被几十亿台设备使用、经过数十年验证的嵌入式数据库之王。现在,仓颉开发者也可以零成本享受它。

**开箱即用,就是这么简单。**

仓库地址:https://gitee.com/longkicode/sqlite3cj

Logo

一站式 AI 云服务平台

更多推荐