主要替代方案 @capacitor-community/sqlite 在 Capacitor 8 上 @capgo/capacitor-fast-sql:一款适用于 iOS、Android 和 Web 的原生 SQLite 插件,SQLCipher 保持可选,查询数据通过本地 HTTP 通道传输,而不是走 Capacitor bridge。本指南会客观对比两者,说明何时该继续用社区插件,并带你完成 code 和用户现有数据库的迁移。
为什么团队在寻找替代方案
@capacitor-community/sqlite 多年来一直是 Capacitor 默认的 SQLite 插件,而且仍在维护(8.1.1 于 2026 年 8 月发布)。人们迁移的原因很具体:
- Bridge 序列化问题。 每个查询结果都要以 JSON 形式经过 Capacitor bridge。小查询没问题,但在同步或搜索时加载数千行数据,损耗就非常明显。
- SQLCipher 始终包含在内。 即使你从不加密,该插件在 iOS 和 Android 上仍依赖 SQLCipher。这会增加原生包体积,也意味着你的二进制文件里包含加密 code,在填写出口合规申报时必须考虑这一点。
- Web 存储模型。 在 Web 端,它使用
jeep-sqlite组件:sql.js 在内存中运行,只有当你调用saveToStore()。你必须记得保存,而且整个数据库都驻留在内存中。 - 庞大的 API 攻击面。 连接封装、一致性检查、只读及非规范连接、同步表、JSON 导入导出。功能强大,但对于“执行 SQL”来说学习成本较高。
- 每个应用一个密码短语。 加密使用由插件存储的单一密钥(
setEncryptionSecret),供应用内所有加密数据库共享。
横向对比
@capacitor-community/sqlite 8.1 |
@capgo/capacitor-fast-sql 8.2 |
|
|---|---|---|
| 平台 | iOS、Android、Web、Electron | iOS、Android、Web |
| 数据路径 | Capacitor 桥接 | 本地 HTTP 服务器,每个连接使用独立 token(原生) |
| Web 引擎 | jeep-sqlite(sql.js + IndexedDB,手动 saveToStore) |
官方 @sqlite.org/sqlite-wasm 使用 OPFS,写入文件 |
| 加密 | 始终捆绑 SQLCipher,由插件管理单一密码 | 可选 SQLCipher,在...上按数据库传递密钥 connect() |
| Swift Package Manager | 可以(Package.swift) |
可以(Package.swift) |
| 模式版本控制 | upgradeStatements / addUpgradeStatement |
通常由您自行管理 PRAGMA user_version |
| JSON 导入/导出,同步表 | 是 | 否 |
| 从资源预填充数据库 | copyFromAssets() |
请自行复制文件 connect() |
| 键值对辅助工具 | 否 | KeyValueStore |
| BLOB | 是 | 是的,作为 Uint8Array |
| ORM 支持 | TypeORM 内置 capacitor 驱动 |
自定义驱动(Kysely 表现良好) |
| 额外的原生配置 | Web:复制 sql-wasm.wasm |
iOS NSAllowsLocalNetworking,Android localhost 明文规则 |
| 维护者 | Capacitor 社区 | Capgo |
何时继续使用社区插件
务实地看。如果符合以下情况,请继续使用:
- 你发布的是 Electron 构建,
- 你使用 TypeORM (其 capacitor 驱动是针对社区插件编写的,参见 在 Capacitor 中使用 TypeORM),
- 你依赖 JSON 导入/导出或同步表 并且不想重新构建它们,
- 你的查询很小,且应用目前运行良好。
何时 Fast SQL 是更好的选择
在以下情况迁移:
- 你加载或写入大型数据集(同步引擎、离线目录、搜索索引、CRDT 日志),
- 你只想对部分数据库加密,密钥来自 Keychain 或 Keystore,
- 你不想在不需要的应用中使用 SQLCipher,
- 你希望通过 OPFS 在 Web 上获得真实的 SQLite 文件,而不是内存副本,
- 你希望简单的 API:连接、查询、执行、事务。
关于 Fast SQL 本身的完整指南,请参阅 如何在 Capacitor 应用中使用 SQLite.
步骤 1:在旧插件旁安装 Fast SQL
保留社区插件一个版本,以便迁移现有数据。
bun add @capgo/capacitor-fast-sql
bunx cap sync
Fast SQL 所需的原生配置:
- iOS:
NSAppTransportSecurity>NSAllowsLocalNetworking=true中的Info.plist. - Android:
network_security_config.xml允许明文传输到localhost以及127.0.0.1,在清单文件中被引用。 - 加密:
net.zetetic:sqlcipher-android在 Android 应用模块和CapgoCapacitorFastSql/SQLCipheriOS 上的 pod 中。详情请参阅 在 Capacitor 中加密 SQLite. - Web 端:
Cross-Origin-Opener-Policy: same-origin以及Cross-Origin-Embedder-Policy: require-corp用于 OPFS 的请求头。
步骤 2:映射 API
打开数据库
修改前:
import { CapacitorSQLite, SQLiteConnection } from '@capacitor-community/sqlite';
const sqlite = new SQLiteConnection(CapacitorSQLite);
const db = await sqlite.createConnection('app', false, 'no-encryption', 1, false);
await db.open();
修改后:
import { FastSQL } from '@capgo/capacitor-fast-sql';
const db = await FastSQL.connect({ database: 'app', walMode: true, performancePresets: true });
FastSQL.connect() 如果数据库已打开,则返回现有连接,因此没有 isConnection / retrieveConnection 复杂的处理。
方法映射
| 社区插件 | 快速 SQL |
|---|---|
createConnection() + open() |
FastSQL.connect({ database }) |
closeConnection(name) |
FastSQL.disconnect(name) |
db.query(sql, values) 返回 { values } |
db.query(sql, params) 返回行 |
db.run(sql, values) 返回 { changes: { changes, lastId } } |
db.run(sql, params) 返回 { rowsAffected, insertId } |
db.execute(multiStatementString) |
一个 db.execute(sql) 每条语句,或 db.executeBatch([...]) |
db.executeSet(set) |
db.executeBatch(ops) 配合 { statement, params } |
beginTransaction / commitTransaction / rollbackTransaction |
db.transaction(async (tx) => ...) 或 beginTransaction() / commit() / rollback() |
addUpgradeStatement / upgradeStatements |
一套 PRAGMA user_version 迁移执行器 |
copyFromAssets() |
复制已打包的 .db 为 Directory.Data 在第一个之前 connect() |
setEncryptionSecret() + mode: 'secret' |
encrypted: true, encryptionKey 开启 connect() |
initWebStore() / saveToStore() |
不需要,OPFS 会持久化写入 |
查询与写入
修改前:
const res = await db.query('SELECT * FROM notes WHERE pinned = ?', [1]);
const notes = res.values ?? [];
const { changes } = await db.run('INSERT INTO notes (title) VALUES (?)', ['Hello']);
const id = changes?.lastId;
修改后:
const notes = await db.query('SELECT * FROM notes WHERE pinned = ?', [1]);
const { insertId } = await db.run('INSERT INTO notes (title) VALUES (?)', ['Hello']);
两个插件返回的行都是以列名为键的对象。
多语句执行
社区 execute() 接受一个字符串,其中包含多个由分隔符隔开的语句 ;。Fast SQL 每次调用只准备一条语句。请拆分你的 SQL,或者更好的做法是将语句保持为数组:
const schema = [
'CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL)',
'CREATE INDEX IF NOT EXISTS idx_notes_title ON notes (title)',
];
await db.executeBatch(schema.map((statement) => ({ statement })));
不要按 ; 进行简单的 split(';') 拆分,如果任何语句包含触发器主体或带有分号的字符串字面量。
将升级语句改为 user_version
社区插件将 schema 版本存储在 PRAGMA user_version 中。如果你保留相同的版本号,在步骤 3 的文件迁移后,现有用户的数据库已经处于正确的版本。
import type { SQLConnection } from '@capgo/capacitor-fast-sql';
// Same content as your old upgrade statements, index 0 = version 1
const versions: string[][] = [
['CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL)'],
['ALTER TABLE notes ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0'],
];
export async function migrate(db: SQLConnection) {
const [{ user_version }] = await db.query('PRAGMA user_version');
for (let v = Number(user_version); v < versions.length; v++) {
await db.transaction(async (tx) => {
for (const statement of versions[v]) await tx.execute(statement);
await tx.execute(`PRAGMA user_version = ${v + 1}`);
});
}
}
在依赖此功能之前,请检查现有安装中的实际 user_version 。如果不确定,请在当前版本中从旧插件记录日志。
预填充数据库
社区插件的 copyFromAssets() 会在首次启动时复制捆绑的数据库。使用 Fast SQL 时,请在连接前将文件放在插件查找的位置。Fast SQL 存储 <name>.db 中的 Documents 在 iOS 上以及应用的 files Android 目录中,两者都是 Directory.Data 中的 @capacitor/filesystem:
import { Directory, Filesystem } from '@capacitor/filesystem';
import { FastSQL } from '@capgo/capacitor-fast-sql';
async function openCatalog() {
const exists = await Filesystem.stat({ path: 'catalog.db', directory: Directory.Data })
.then(() => true)
.catch(() => false);
if (!exists) {
const blob = await (await fetch('/assets/catalog.db')).blob();
const base64 = await new Promise<string>((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve((reader.result as string).split(',')[1]);
reader.readAsDataURL(blob);
});
await Filesystem.writeFile({ path: 'catalog.db', data: base64, directory: Directory.Data });
}
return FastSQL.connect({ database: 'catalog', readOnly: true });
}
对于大文件,请分块下载或发送压缩文件,并使用文件插件的原生复制功能,而不是在 JavaScript 中使用 base64。
步骤 3:迁移现有用户数据
更新您应用的用户拥有一个由社区插件创建的数据库。它位于不同的路径,且名称不同:
| 社区插件 | 快速 SQL | |
|---|---|---|
| iOS | Documents/<name>SQLite.db (默认 iosDatabaseLocation) |
Documents/<name>.db |
| Android | databases/<name>SQLite.db |
files/<name>.db |
方案 A:复制文件(未加密数据库)
速度最快,且保留所有内容:索引、触发器、 user_version.
import { CapacitorSQLite, SQLiteConnection } from '@capacitor-community/sqlite';
import { Directory, Filesystem } from '@capacitor/filesystem';
import { Preferences } from '@capacitor/preferences';
export async function moveCommunityDatabase(name: string): Promise<void> {
const done = await Preferences.get({ key: `sqlite-moved:${name}` });
if (done.value === '1') return;
const sqlite = new SQLiteConnection(CapacitorSQLite);
const exists = (await sqlite.isDatabase(name)).result;
if (exists) {
const conn = await sqlite.createConnection(name, false, 'no-encryption', 1, false);
await conn.open();
await conn.execute('PRAGMA wal_checkpoint(TRUNCATE)', false).catch(() => undefined);
const { url } = await conn.getUrl();
await sqlite.closeConnection(name, false);
if (url) {
await Filesystem.copy({ from: url, to: `${name}.db`, toDirectory: Directory.Data });
}
}
await Preferences.set({ key: `sqlite-moved:${name}`, value: '1' });
}
在首次 FastSQL.connect({ database: name })之前调用。注意:
getUrl()(iOS 和 Android)返回数据库文件 URL。传入完整的file://URL 且不带directory,Filesystem 插件即可从沙箱内的任意路径读取。- 先关闭连接并执行 checkpoint,可确保最近的写入不会残留在
-wal文件。 - 保留旧文件一个版本,待确认无误后,使用社区连接的
delete()方法将其删除。然后移除社区插件。
方案 B:在 JavaScript 中复制行(加密数据库或 schema 变更)
当源数据库使用社区插件存储的密钥加密时,最安全的做法是让该插件解密并将行数据复制到新的 Fast SQL 数据库中。这样还可以使用安全存储中的新每数据库密钥进行加密。
import { CapacitorSQLite, SQLiteConnection } from '@capacitor-community/sqlite';
import { FastSQL } from '@capgo/capacitor-fast-sql';
import { Preferences } from '@capacitor/preferences';
export async function copyRows(name: string, target: { encryptionKey?: string }) {
// Record which destination mode was migrated, so a later call with a different target fails loudly
const mode = target.encryptionKey ? 'encrypted' : 'plain';
const done = await Preferences.get({ key: `sqlite-copied:${name}` });
if (done.value === mode) return;
if (done.value) throw new Error(`${name} was already copied as ${done.value}, not ${mode}`);
const sqlite = new SQLiteConnection(CapacitorSQLite);
const src = await sqlite.createConnection(name, true, 'secret', 1, false);
await src.open();
try {
const dst = await FastSQL.connect({
database: name,
encrypted: Boolean(target.encryptionKey),
encryptionKey: target.encryptionKey,
});
const objects = ((await src.query(
`SELECT type, name, sql FROM sqlite_master
WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%'`,
)).values ?? []) as { type: string; name: string; sql: string }[];
const tables = objects.filter((o) => o.type === 'table');
const others = objects.filter((o) => o.type !== 'table');
const version = (await src.query('PRAGMA user_version')).values?.[0]?.user_version ?? 0;
await dst.transaction(async (tx) => {
for (const t of tables) await tx.execute(t.sql);
for (const t of tables) {
const rows = (await src.query(`SELECT * FROM "${t.name}"`)).values ?? [];
if (!rows.length) continue;
const cols = Object.keys(rows[0]);
const statement = `INSERT INTO "${t.name}" (${cols.map((c) => `"${c}"`).join(', ')}) VALUES (${cols.map(() => '?').join(', ')})`;
await tx.executeBatch(rows.map((r) => ({ statement, params: cols.map((c) => r[c]) })));
}
for (const o of others) await tx.execute(o.sql);
await tx.execute(`PRAGMA user_version = ${Number(version)}`);
});
// Only mark the copy as done once the transaction has committed
await Preferences.set({ key: `sqlite-copied:${name}`, value: mode });
} finally {
await sqlite.closeConnection(name, false);
}
}
使用 'no-encryption' 以及 false 中的 createConnection 如果源数据库未加密。社区插件需要 iosIsEncryption / androidIsEncryption 中的 capacitor.config 用于其加密模式,如果你之前使用过,应该已经具备。对于大表,请使用 WHERE rowid > ? ORDER BY rowid LIMIT 5000.
两个插件都使用 SQLCipher 4 默认设置,因此加密文件也可以直接使用相同的密码在 Fast SQL 中打开。不过,社区插件在内部管理该密码,而行复制可以避免依赖这一细节。
步骤 4:移除旧插件
在迁移就位后发布一两个版本:
bun remove @capacitor-community/sqlite jeep-sqlite
bunx cap sync
移除 sql-wasm.wasm 从你的资源文件和任何 jeep-sqlite 设置仅需 main.ts。移除该插件也会移除其对 SQLCipher 的依赖,因此如果你不选择 Fast SQL 的加密功能,iOS 和 Android 的二进制文件体积会更小。
数据迁移 code 是 JavaScript,因此可以随 Capgo 实时更新一起发布,但添加 Fast SQL 的版本涉及原生变更,需要商店构建。请先发布原生构建。 Capgo 构建 如果你手边没有 Mac,也可以在云端构建(自 2026 年 4 月起,App Store 上传需要 Xcode 26 或更高版本)。
故障排除
Fast SQL 可以连接,但在 iOS 上所有查询都会失败。 NSAllowsLocalNetworking 在应用的 Info.plist中缺失,或者添加该插件后未重新构建应用。
Android: CLEARTEXT communication to localhost not permitted. 将 localhost 添加到 domain-config 你的网络安全配置中。如果你已有用于 SSL pinning 的配置,请将两者合并。
我的 schema 只创建了一部分。 你向以下参数传入了包含多条语句的字符串: execute()。请将其拆分为数组。
INSERT ... RETURNING 在 Android 上不返回任何内容。 在 Fast SQL 8.2 中,Android 仅对以以下内容开头的语句返回行: SELECT, PRAGMA 或 EXPLAIN. 用于 insertId 来自 run().
在 Android 上读取时,Boolean 值不匹配。 传入 1/0 而不是 true/false 在查询参数中。Android 会将读取查询参数绑定为文本。
复制的数据库为空。 该社区插件配置了自定义 iosDatabaseLocation,或者源名称不一致。记录由以下内容返回的 URL getUrl().
旧插件中的 Web 数据已丢失。 社区插件将 Web 数据存储在 IndexedDB 中, jeep-sqlite而 Fast SQL 使用 OPFS。请从后端重新同步 Web 客户端,或使用旧插件的导出功能, exportToJson 再将数据行导入 Fast SQL。
其他方案
@capgo/capacitor-data-storage-sqlite如果你真正需要的是基于 SQLite 的字符串键值存储。请参阅 Capacitor 中的键值存储.- 在其上使用 ORM 或查询构建器: Kysely 可通过一个简短的自定义驱动与 Fast SQL 配合使用。 Drizzle 目前更适合社区插件。