引言
需要持久化存储时,SQLite 是嵌入式系统的默认答案:单文件、零服务进程、跨平台、ACID 事务。Zig 调用 SQLite 极其自然——@cImport 直接导入 C 头文件,语言层面的 C ABI 互操作让 sqlite3_* 函数调用与在 C 中一样直接,却没有手写胶水代码的负担。
本文从 @cImport 绑定讲起,覆盖数据库打开、参数绑定与预编译语句(防止 SQL 注入)、结果集遍历、事务与 WAL 模式、连接与分配器生命周期管理,最后给出把 SQLite 行映射到 Zig 结构体的完整工程模式。
前置:/zig-c-interoperability/(C ABI 互操作)、/zig-advanced-ffi/(类型映射与内存布局)、/zig-json-serialization/(结果映射到结构体)。
目录
- 1. 为什么 SQLite:嵌入式数据库的取舍
- 2. @cImport 绑定 sqlite3
- 3. 打开数据库与初始化
- 4. 参数绑定与预编译语句
- 5. 结果集遍历与列读取
- 6. 事务与 WAL 模式
- 7. 分配器与连接生命周期
- 8. 行映射到结构体的工程模式
- 9. 速查表
- 10. 一句话记忆
- 延伸阅读
1. 为什么 SQLite:嵌入式数据库的取舍
| 方案 | 优势 | 代价 |
|---|---|---|
| SQLite | 单文件、零服务、ACID、超广兼容 | 单写多读、不适合海量并发写 |
| 原生文件 | 最快、最简单 | 无事务、无索引、易损坏 |
| 外部 DB(PG/MySQL) | 高并发、分布式 | 要服务进程、部署复杂 |
选型判断:
□ 单进程/单机应用 → SQLite(配置、元数据、日志、缓存)
□ 高并发写(>100 QPS 写)→ 考虑 WAL + 适当批处理,仍不够再换服务型 DB
□ 纯读缓存/只读库 → SQLite 性能极佳(WAL 模式读不阻塞)
Zig 的结合点:Zig 产物是单静态二进制,SQLite 是单库文件——「一个可执行文件 + 一个 .db 文件」构成完整应用,分发极简。
2. @cImport 绑定 sqlite3
Zig 通过 @cImport 直接把 SQLite 头文件变成 Zig 声明:
const std = @import("std");
const c = @cImport({
@cInclude("sqlite3.h");
});
链接 SQLite:SQLite 是 C 库,需要链接。两种方式:
# 方式 A:系统已装 libsqlite3
zig build-exe src/db.zig -lc -lsqlite3
# 方式 B:源码单文件直接编译(推荐,可控版本)
zig build-exe src/db.zig sqlite3.c -I. -lpthread -lm
在 build.zig 中:
exe.linkSystemLibrary("sqlite3");
exe.linkLibC();
版本注意:用官方 sqlite3.h 声明即得全套 API;Zig 的 C 导入会自动生成类型安全的函数签名([*c]sqlite3 指针等)。
3. 打开数据库与初始化
const std = @import("std");
const c = @cImport({ @cInclude("sqlite3.h"); });
const Db = struct {
handle: *c.sqlite3,
fn open(path: []const u8) !Db {
var db: ?*c.sqlite3 = null;
// 参数: 文件路径(以 0 结尾), 出参 db, 标志, zVfs
const rc = c.sqlite3_open(path.ptr, &db);
if (rc != c.SQLITE_OK) {
const msg = if (db) |d| c.sqlite3_errmsg(d) else "unknown";
return error.SqliteOpenFailed;
}
return .{ .handle = db.? };
}
fn deinit(self: *Db) void {
_ = c.sqlite3_close(self.handle);
}
};
常用打开标志:
| 标志 | 作用 |
|---|---|
SQLITE_OPEN_READWRITE | 读写打开 |
SQLITE_OPEN_CREATE | 不存在则创建 |
SQLITE_OPEN_READONLY | 只读(配合 FTS 索引等) |
SQLITE_OPEN_FULLMUTEX | 线程安全模式 |
路径必须 NUL 结尾:Zig 用 path.ptr 前要确保 [:0]const u8(.z 切片或 sentinel)。
4. 参数绑定与预编译语句
绝不拼接 SQL——用预编译语句 + 参数绑定防 SQL 注入:
const stmt: *c.sqlite3_stmt = blk: {
var s: ?*c.sqlite3_stmt = null;
const rc = c.sqlite3_prepare_v2(db.handle,
"INSERT INTO users(name, email) VALUES(?, ?)", -1, &s, null);
if (rc != c.SQLITE_OK) return error.PrepareFailed;
break :blk s.?;
};
defer c.sqlite3_finalize(stmt);
// 绑定参数(索引从 1 开始)
_ = c.sqlite3_bind_text(stmt, 1, name.ptr, @intCast(name.len), c.SQLITE_TRANSIENT);
_ = c.sqlite3_bind_text(stmt, 2, email.ptr, @intCast(email.len), c.SQLITE_TRANSIENT);
// 执行
if (c.sqlite3_step(stmt) != c.SQLITE_DONE) return error.ExecFailed;
绑定函数矩阵:
| 函数 | 绑定类型 |
|---|---|
sqlite3_bind_int / bind_int64 | 整数 |
sqlite3_bind_double | 浮点 |
sqlite3_bind_text | 文本(utf8) |
sqlite3_bind_blob | 二进制 |
sqlite3_bind_null | NULL |
sqlite3_bind_parameter_index | 按名称绑定(:name) |
SQLITE_TRANSIENT:告诉 SQLite 立即拷贝数据(因为 Zig 缓冲可能马上释放);如果缓冲在语句生命周期内稳定,可用 SQLITE_STATIC 省一次拷贝。
5. 结果集遍历与列读取
SELECT 用 sqlite3_step 循环,SQLITE_ROW 表示一行:
const stmt: *c.sqlite3_stmt = try prepare(db, "SELECT id, name, age FROM users WHERE age > ?", .{18});
while (true) {
const rc = c.sqlite3_step(stmt);
if (rc == c.SQLITE_ROW) {
const id: i64 = c.sqlite3_column_int64(stmt, 0);
const name_ptr = c.sqlite3_column_text(stmt, 1);
const name_len: usize = @intCast(c.sqlite3_column_bytes(stmt, 1));
const name = name_ptr[0..name_len]; // 切片自列缓冲区
const age: i64 = c.sqlite3_column_int64(stmt, 2);
std.debug.print("id={d} name={s} age={d}\n", .{ id, name, age });
} else if (rc == c.SQLITE_DONE) {
break; // 无更多行
} else {
return error.QueryFailed;
}
}
列读取要点:
□ 列索引从 0 开始
□ column_text 返回指向语句内部缓冲的指针——生命周期到下一次 step/finalize
□ 需要用列值时立即拷贝或用列缓冲区切片的副本
□ column_count 查询列数;column_name 取列名
注意:SQLite 内部缓冲在
sqlite3_step下一次调用后失效,跨step保留必须拷贝。
6. 事务与 WAL 模式
事务保证原子性——批量写入放进一个事务,避免逐条提交的性能损耗与中途崩溃的不一致:
fn insertBatch(db: *Db, rows: []const Row) !void {
_ = try exec(db, "BEGIN");
errdefer _ = exec(db, "ROLLBACK"); // 出错自动回滚
for (rows) |row| try insertRow(db, row);
_ = try exec(db, "COMMIT");
}
WAL 模式(推荐生产开启):写不阻塞读、崩溃安全、性能更好:
_ = try exec(db, "PRAGMA journal_mode = WAL");
_ = try exec(db, "PRAGMA synchronous = NORMAL"); // WAL 下 NORMAL 足够安全且快
_ = try exec(db, "PRAGMA busy_timeout = 5000"); // 写锁等待 5s,不报错
PRAGMA 调优对照:
| PRAGMA | 作用 |
|---|---|
journal_mode=WAL | 写-读并发分离 |
synchronous=NORMAL | WAL 下安全性与性能平衡 |
cache_size=-64000 | 64MB 页缓存(负数=KB) |
foreign_keys=ON | 外键约束(默认关!) |
busy_timeout | 锁等待时长 |
记忆:事务批量写 + WAL + busy_timeout,是 SQLite 生产级三件套。
7. 分配器与连接生命周期
连接生命周期:一个进程通常一个连接(SQLite 线程安全取决于编译选项与打开标志):
const Database = struct {
handle: *c.sqlite3,
allocator: std.mem.Allocator,
fn init(allocator: std.mem.Allocator, path: [:0]const u8) !Database {
var db: ?*c.sqlite3 = null;
if (c.sqlite3_open(path.ptr, &db) != c.SQLITE_OK) return error.OpenFailed;
// 启用完整互斥,允许跨线程访问同一连接
_ = c.sqlite3_db_config(db, c.SQLITE_DBCONFIG_ENABLE_FTS3, 0, 0);
return .{ .handle = db.?, .allocator = allocator };
}
fn deinit(self: *Database) void {
_ = c.sqlite3_close(self.handle);
}
};
内存策略:
| 策略 | 场景 |
|---|---|
每个查询结果分配到 allocator,deinit 统一释放 | 常规应用 |
sqlite3_set_authorizer + 只读查询 | 安全读 |
| 大结果分批游标 | 避免全量入内存 |
| 连接池(多线程) | 每线程独立连接最稳 |
错误检查:sqlite3_errcode / sqlite3_errmsg 拿到原始错误,映射到 Zig 错误联合再抛给调用方。
8. 行映射到结构体的工程模式
把查询结果组装成 Zig 结构体,是数据访问层最常用的模式:
const User = struct {
id: i64,
name: []const u8,
age: u8,
};
fn queryUsers(allocator: std.mem.Allocator, db: *Db, minAge: u8) ![]User {
const stmt = try prepare(db, "SELECT id, name, age FROM users WHERE age >= ?", &.{minAge});
defer c.sqlite3_finalize(stmt);
var list = std.ArrayList(User).init(allocator);
errdefer list.deinit();
while (true) {
const rc = c.sqlite3_step(stmt);
if (rc == c.SQLITE_ROW) {
const name_ptr = c.sqlite3_column_text(stmt, 1);
const name_len: usize = @intCast(c.sqlite3_column_bytes(stmt, 1));
try list.append(.{
.id = c.sqlite3_column_int64(stmt, 0),
.name = try allocator.dupe(u8, name_ptr[0..name_len]), // 拷贝!
.age = @intCast(c.sqlite3_column_int64(stmt, 2)),
});
} else if (rc == c.SQLITE_DONE) {
break;
} else {
return error.QueryFailed;
}
}
return list.toOwnedSlice();
}
调用方负责释放:
const users = try queryUsers(allocator, &db, 18);
defer allocator.free(users);
for (users) |u| {
std.debug.print("name={s} age={d}\n", .{ u.name, u.age });
}
工程注意:
name从 SQLite 内部缓冲拷贝到 allocator 内存(dupe),否则释放时机不可控。- 批量查询返回切片由调用方
free——所有权明确、无泄漏。 - 配合
std.json可一键把[]User序列化成 API 响应。
9. 速查表
| 需求 | 手段 |
|---|---|
| 绑定 SQLite | @cImport + -lsqlite3 或编译 sqlite3.c |
| 打开库 | sqlite3_open / sqlite3_open_v2 |
| 防注入 | 预编译语句 + 参数绑定(sqlite3_bind_*) |
| 执行写 | sqlite3_step → SQLITE_DONE |
| 读取行 | sqlite3_step → SQLITE_ROW + sqlite3_column_* |
| 事务 | BEGIN/COMMIT/ROLLBACK + errdefer |
| 并发读写 | PRAGMA journal_mode=WAL + busy_timeout |
| 外键 | PRAGMA foreign_keys=ON |
| 结果映射 | 遍历列 → 拷贝到 allocator → 结构体切片 |
| 清理 | sqlite3_finalize / sqlite3_close |
10. 一句话记忆
Zig 调 SQLite = @cImport + 预编译语句防注入 + 列拷贝到 Allocator 管理所有权;事务批量写、WAL 并发读、busy_timeout 防锁死——嵌入式存储的可靠三件套齐活。
延伸阅读
- /zig-c-interoperability/ — C ABI 与 @cImport 机制
- /zig-advanced-ffi/ — 结构体布局与内存所有权
- /zig-memory-management/ — allocator 依赖注入模式
- /zig-json-serialization/ — 结果结构体序列化为 JSON
- [[zig]] — Zig 系统编程专题
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。