要在 Capacitor 应用中使用 SQLite,请安装一个原生 SQLite 插件,例如 @capgo/capacitor-fast-sql,在启动时打开一次数据库,用版本化迁移创建 schema,然后用参数化 SQL 读写。本指南会完整讲解 Capacitor 8 在 iOS、Android 和 Web 上的这套配置,包括事务、加密、文件位置,以及你最常遇到的错误。
如果只需要少量设置,SQLite 就大材小用了。如果你要存储列表、记录,或者需要筛选、排序、同步的数据,它才是合适的工具。
何时该选择 SQLite 作为存储方案
Capacitor 应用有多种数据存储位置,它们解决不同的问题:
| 存储空间 | 适用场景 | 限制 |
|---|---|---|
localStorage / IndexedDB |
临时缓存 | 存在于 WebView 中,系统可能会清除它,localStorage 不支持真正的查询 |
@capacitor/preferences |
少量小型设置(主题、语言) | 仅支持字符串值,不适合存储大量数据,且未加密 |
| Keychain / Keystore 存储 | Token、加密密钥 | 仅限小型密钥 |
文件 (@capacitor/filesystem) |
图片、PDF、导出文件 | 不支持查询 |
| SQLite | 记录、离线数据、同步队列、搜索 | 由你管理 schema 和迁移 |
需要以下任意一项时,就选 SQLite:离线优先数据、超过几百条记录、筛选和排序、表间关系、原子性多步写入,或结构化数据的静态加密。
2026 年如何选择 Capacitor SQLite 插件
在 Capacitor 8 上,原生 SQLite 有三种切实可行的方案:
@capgo/capacitor-fast-sql |
@capacitor-community/sqlite |
@capgo/capacitor-data-storage-sqlite |
|
|---|---|---|---|
| 用途 | 通用 SQL 数据库 | 通用 SQL 数据库 | SQLite 上的字符串键值存储 |
| 平台 | iOS、Android、Web | iOS、Android、Web、Electron | iOS、Android、Web、Electron |
| Web 引擎 | 官方 SQLite Wasm + OPFS | jeep-sqlite(sql.js + IndexedDB) | IndexedDB |
| 数据传输 | 本地 HTTP 通道,绕过 bridge | Capacitor 桥接 | Capacitor 桥接 |
| 加密 | 可选 SQLCipher(iOS、Android) | 始终内置 SQLCipher | SQLCipher,口令在 config 中配置 |
| 行格式 | 以列为键的对象 | 以列为键的对象 | 字符串 |
| ORM 支持 | 自定义驱动(Kysely、Drizzle) | TypeORM 内置驱动 | 无 |
本指南使用 Fast SQL。它由 Capgo 维护,跟踪 Capacitor 主要版本(Capacitor 8 对应插件 v8),将 SQLCipher 设为可选,并避免通过 bridge 序列化大型结果集。如果你正在使用社区插件,请阅读我们的 对比与迁移指南 首先。
安装 Fast SQL
bun add @capgo/capacitor-fast-sql
bunx cap sync
Fast SQL 会启动一个绑定到 localhost 的小型 HTTP 服务器,并通过每个连接的 bearer token 向其发送 SQL。两个平台默认都阻止明文 HTTP,因此请仅对 loopback 允许。
iOS:允许本地网络
将此添加到 ios/App/App/Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
NSAllowsLocalNetworking 仅覆盖 loopback 和本地地址。它不会放宽你实际 API 调用的 App Transport Security 设置。
Android:允许到 localhost 的明文流量
创建 android/app/src/main/res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">localhost</domain>
<domain includeSubdomains="false">127.0.0.1</domain>
</domain-config>
</network-security-config>
在以下位置引用 android/app/src/main/AndroidManifest.xml:
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
如果你已有网络安全配置(例如用于 SSL 固定),请将 domain-config 块添加到其中,而不是再创建一个文件。
只打开一次数据库
创建一个单一模块来管理连接。反复打开同一个数据库很浪费,而且你需要一个统一的地方来运行迁移。
// src/db/index.ts
import { FastSQL, type SQLConnection } from '@capgo/capacitor-fast-sql';
import { migrate } from './migrate';
let dbPromise: Promise<SQLConnection> | null = null;
export function getDb(): Promise<SQLConnection> {
if (!dbPromise) {
dbPromise = (async () => {
const db = await FastSQL.connect({
database: 'notes',
walMode: true, // PRAGMA journal_mode = WAL
performancePresets: true, // synchronous=NORMAL, busy_timeout, foreign_keys=ON
});
await migrate(db);
return db;
})().catch((error) => {
dbPromise = null; // let the next call retry instead of reusing the failure
throw error;
});
}
return dbPromise;
}
walMode 以及 performancePresets 从插件版本 8.0.49 起可用。 performancePresets 也会启用 foreign_keys,而 SQLite 默认关闭该选项,因此 ON DELETE CASCADE 才能真正生效。
调用 getDb() 在你需要数据库的任何地方。首次调用会打开并迁移数据库,后续调用会复用同一个 Promise。如果打开或迁移失败,下一次调用会重试。
使用 PRAGMA user_version 进行 Schema 迁移
Fast SQL 提供的是原生 SQL,而非迁移框架。这没问题,因为 SQLite 的文件头中已经有一个版本字段: PRAGMA user_version。维护一个有序的步骤数组,只应用缺失的部分。
// src/db/migrate.ts
import type { SQLConnection } from '@capgo/capacitor-fast-sql';
// Index 0 = version 1, index 1 = version 2, ...
// Never edit a step after it shipped. Add a new one.
const steps: string[][] = [
[
`CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT,
created_at INTEGER NOT NULL
)`,
],
[
`ALTER TABLE notes ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0`,
`CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes (created_at)`,
],
];
export async function migrate(db: SQLConnection): Promise<void> {
const rows = await db.query('PRAGMA user_version');
const current = Number(rows[0]?.user_version ?? 0);
for (let version = current; version < steps.length; version++) {
await db.transaction(async (tx) => {
for (const statement of steps[version]) {
await tx.execute(statement);
}
// PRAGMA does not accept bound parameters, the value is a trusted integer
await tx.execute(`PRAGMA user_version = ${version + 1}`);
});
}
}
确保安全的规则:
- 每条
execute()call。两种原生实现都只准备字符串中的第一条语句,因此"CREATE ...; CREATE ..."会静默执行其中一条。 - 每个版本都在事务中运行,因此中途崩溃会使数据库停留在上一版本,下次启动时会重新执行该步骤。
- 优先采用增量变更(新表、新的可空列)。SQLite 支持
ALTER TABLE ... DROP COLUMN以及RENAME COLUMN在较新版本中已支持,但旧版 Android 的系统 SQLite 可能版本较旧,因此改造表结构的可移植做法是:创建新表、复制数据、删除旧表、重命名。
迁移与 Live Updates
如果你通过以下方式发布 JavaScript 变更: Capgo 实时更新你的迁移 code 会随 bundle 一起发布。这很方便,因为 schema 变更无需通过应用商店发布。这也意味着回滚可能会把旧版 bundle 覆盖到新版 schema 上。保持迁移向后兼容(添加列,不要在停止使用这些列的同一次发布中重命名或删除它们),旧版 bundle 就能继续正常工作。
读写数据
SQLConnection 为你提供了四个常用的方法:
| 方法 | 返回 | 用于 |
|---|---|---|
query(sql, params) |
SQLRow[] |
SELECT 和 PRAGMA 读取 |
run(sql, params) |
{ rowsAffected, insertId } |
INSERT, UPDATE, DELETE |
execute(sql, params) |
{ rows, rowsAffected, insertId } |
任意内容,完整结果 |
executeBatch(ops) |
SQLResult[] |
一次往返执行多条语句 |
始终使用 ? 占位符来绑定值。切勿用字符串拼接用户输入来构建 SQL。
// src/db/notes.ts
import { getDb } from './index';
export interface Note {
id: number;
title: string;
body: string | null;
created_at: number;
pinned: number;
}
export async function addNote(title: string, body: string | null): Promise<number> {
const db = await getDb();
const { insertId } = await db.run(
'INSERT INTO notes (title, body, created_at) VALUES (?, ?, ?)',
[title, body, Date.now()],
);
if (insertId == null) throw new Error('Insert did not return an id');
return insertId;
}
export async function listNotes(search = ''): Promise<Note[]> {
const db = await getDb();
const rows = await db.query(
'SELECT id, title, body, created_at, pinned FROM notes WHERE title LIKE ? ORDER BY pinned DESC, created_at DESC',
[`%${search}%`],
);
return rows as unknown as Note[];
}
export async function setPinned(id: number, pinned: boolean): Promise<void> {
const db = await getDb();
await db.run('UPDATE notes SET pinned = ? WHERE id = ?', [pinned ? 1 : 0, id]);
}
export async function deleteNote(id: number): Promise<number> {
const db = await getDb();
const { rowsAffected } = await db.run('DELETE FROM notes WHERE id = ?', [id]);
return rowsAffected;
}
行数据以列名为键的普通对象形式返回,因此无需进行 columns/values 映射步骤。SQLite 没有布尔类型,因此存储 1 以及 0 并在数据层边缘进行转换,例如 setPinned 。
事务与批量写入
将每个多步写入操作包裹在事务中。这能保持数据一致性,而且速度更快,因为 SQLite 每次 commit 只同步一次磁盘,而不是每条语句都同步一次。
import { getDb } from './index';
export async function importNotes(items: { title: string; body: string }[]) {
const db = await getDb();
const now = Date.now();
await db.transaction(async (tx) => {
await tx.executeBatch(
items.map((item) => ({
statement: 'INSERT INTO notes (title, body, created_at) VALUES (?, ?, ?)',
params: [item.title, item.body, now],
})),
);
});
}
transaction() 当回调 resolve 时提交,抛出异常时回滚。如果需要手动控制,还可以使用 beginTransaction(), commit() 以及 rollback() 在连接上。
每个数据库的所有调用共享同一个原生连接。事务开启时,其他写入同一数据库的 code 会在该事务内运行。请保持事务简短,并避免在事务内等待网络请求。
存储二进制数据
传入一个 Uint8Array 并得到一个 Uint8Array 作为返回。插件会对其进行 base64 编码以便传输,并存储为真正的 BLOB。
const db = await getDb();
await db.execute('CREATE TABLE IF NOT EXISTS thumbnails (note_id INTEGER PRIMARY KEY, data BLOB)');
await db.run('INSERT OR REPLACE INTO thumbnails (note_id, data) VALUES (?, ?)', [noteId, bytes]);
const [row] = await db.query('SELECT data FROM thumbnails WHERE note_id = ?', [noteId]);
const image = row?.data as Uint8Array | undefined;
大型媒体(照片、视频)请保留为文件,只在 SQLite 中存路径。BLOB 适合缩略图、小型附件和向量嵌入。
加密数据库
Fast SQL 在 iOS 和 Android 上支持 SQLCipher。该功能需手动启用,不需要的 App 不会打包它:
const db = await FastSQL.connect({
database: 'vault',
encrypted: true,
encryptionKey: keyFromSecureStorage,
});
在 Android 上,你需要添加 net.zetetic:sqlcipher-android 到 App 的 build.gradle。在 iOS 上,将 pod 切换为 CapgoCapacitorFastSql/SQLCipher 子规格。生成密钥、将其存储在 Keychain 或 Keystore、加密现有明文数据库以及轮换密钥,均在以下内容中逐步说明: 如何在 Capacitor 中加密 SQLite 数据库。Web 端不支持加密。
Web 端支持
在 Web 端,Fast SQL 使用官方的 @sqlite.org/sqlite-wasm 进行 build,并将文件存储在 Origin Private File System (OPFS) 中。它是真正的 SQLite 文件,而不是保存在内存中再刷写到 IndexedDB 的副本。
OPFS 需要跨源隔离 (cross-origin isolation),因此你的开发服务器和生产主机必须发送两个 Header:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
使用 Vite 时:
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
server: {
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
},
optimizeDeps: {
exclude: ['@sqlite.org/sqlite-wasm'],
},
});
如果缺少这些 Header,插件会记录一条警告并回退到非持久化数据库:一切都能正常工作,直到你重新加载页面。 CapgoCapacitorFastSql.configureWeb({ useOpfs, worker }) 让你在首次使用前修改默认设置 connect(),并且在 native 端是 no-op。
require-corp 还会拦截未发送 CORP 或 CORS Header 的跨源图片和脚本,因此开启此功能时请测试第三方嵌入内容。
数据库文件的存储位置
| 平台 | 位置 | 备注 |
|---|---|---|
| iOS | Documents/<name>.db |
默认包含在 iCloud 和设备备份中 |
| Android | files/<name>.db 位于应用的内部存储中 |
受 Android Auto Backup 规则限制 |
| Web 端 | 页面 Origin 下的 OPFS | 用户清除网站数据时会被清除 |
这会带来两个实际影响。首先,卸载应用会在两个移动平台上删除该文件。其次,备份可以把数据库文件恢复到新设备上。这通常正是你想要的结果,但如果数据库加密所用的密钥没有随备份一起迁移,恢复后的文件就无法打开。加密指南介绍了如何处理这种情况。
性能检查清单
- 开启
walMode. 读操作不再阻塞写操作。 - 在事务中批量写入,使用
executeBatch. 这是导入和同步性能提升最大的一招。 - 为筛选或排序的列添加索引,然后用 --- 检查
EXPLAIN QUERY PLAN. - 只查询需要的列。大
TEXT以及BLOB列即使通过 HTTP 通道传输也会耗费传输时间。 - 用 --- 分页
WHERE created_at < ? ORDER BY created_at DESC LIMIT 50而不是大OFFSET值。 - 删除大量数据后,请运行
VACUUM偶尔运行以收缩文件。该操作会重写整个数据库,所以请在空闲时执行。
故障排除
SQL execution failed 在 iOS 上连接后立即执行。 本地 HTTP 请求被拦截。请检查 NSAllowsLocalNetworking 是否位于正确的 Info.plist 并重新构建原生应用,仅靠 live reload 是不够的。
CLEARTEXT communication to localhost not permitted 在 Android 上。 缺少网络安全配置,或未在 manifest 中引用。
仅执行了第一条语句。 你在一个字符串中传入了多条语句。请拆分,或使用 executeBatch.
no such table 更新后。 迁移未运行,或者针对的是另一个数据库名称。请记录 PRAGMA user_version 启动日志,并与你的 steps 数组对比。
在 Android 上,针对布尔值或计算值的过滤条件永远不会匹配。 对于读查询,Android 实现会把参数绑定为文本。与具有 INTEGER 亲和性的列比较仍然有效,因为 SQLite 会转换文本;但与表达式比较时(例如 json_extract(...) = ? 或 count(*) > ?)会将文本与数字进行比较。请传入 1/0 而不是 true/false,并使用 CAST(? AS INTEGER) 用于与表达式比较。
INSERT ... RETURNING 在 Android 上不会返回任何行。 在当前插件版本中,Android 只会为以以下内容开头的语句返回行: SELECT, PRAGMA 或 EXPLAIN. 用于 insertId 来自 run() 然后选择该行。以以下内容开头的查询也一样: WITH:把它们包起来,让它们以以下内容开头 SELECT,或者尽早进行 Android 端测试。
浏览器刷新后数据会消失。 OPFS 不可用,几乎总是因为部署的主机缺少 COOP/COEP 响应头。
Encryption is not available in this build 在 iOS 上协调。 你传入了 encrypted: true 而未切换到 SQLCipher 子规格。
深入探索
对于小型应用,使用原生 SQL 就足够了。但当表数量增多时,引入类型层会更有帮助:
- 结合 Capacitor 和 SQLite 使用 Kysely:类型化查询构建器,搭配一个小型自定义驱动来支持 Fast SQL。
- 结合 Capacitor 和 SQLite 使用 Drizzle ORM:用 TypeScript 定义 schema,并自动生成 migrations。
- 结合 Capacitor 和 SQLite 使用 TypeORM:decorators 和 repositories。
- Capacitor 中的键值存储方案:当 Preferences 或 SQLite 上的 KV 表就够用时。
插件参考文档位于 Fast SQL 文档 以及 plugin 页面。Fast SQL 在添加或更改配置时需要进行原生重新构建。如果你不想为此维护一台 Mac, Capgo 构建 可以在云端生成已签名的 iOS 和 Android 二进制文件。自 2026 年 4 月起,App Store 上传必须使用 Xcode 26 或更高版本构建,因此请在下次发布前检查你的构建机器。