跳转至正文

如何在 Capacitor 应用中使用 SQLite(2026 指南)

学习如何在 Capacitor 应用中使用 SQLite:选择插件、打开数据库、运行迁移、事务、加密及 Web 支持,并附带可运行的 code。

Capacitor logo,用于 Capacitor 应用中使用 SQLite 的指南

要在 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 就足够了。但当表数量增多时,引入类型层会更有帮助:

插件参考文档位于 Fast SQL 文档 以及 plugin 页面。Fast SQL 在添加或更改配置时需要进行原生重新构建。如果你不想为此维护一台 Mac, Capgo 构建 可以在云端生成已签名的 iOS 和 Android 二进制文件。自 2026 年 4 月起,App Store 上传必须使用 Xcode 26 或更高版本构建,因此请在下次发布前检查你的构建机器。

为 Capacitor 应用提供 Live Updates

当 Web 层出现线上 Bug 时,通过 Capgo 发布修复,而不用等 App Store 审核好几天。用户会在后台收到更新,原生变更则仍走正常审核流程。

博客最新动态

Capgo 为你提供打造真正专业的移动应用所需的最有价值的洞察。