Capacitor アプリで SQLite を使うには、ネイティブ SQLite プラグイン(例: @capgo/capacitor-fast-sql)をインストールし、起動時にデータベースを一度だけ開き、バージョン管理されたマイグレーションでスキーマを作成し、パラメータ化された SQL で読み書きします。本ガイドでは、Capacitor 8 における iOS、Android、Web での完全なセットアップ手順を、トランザクション、暗号化、ファイルの保存場所、および発生しやすいエラーを含めて解説します。
設定項目が数個だけなら、SQLite は大げさです。リストやレコードなど、フィルタリング、ソート、同期が必要なデータを保存するなら、SQLite が最適なツールです。
SQLiteがストレージとして適切なケース
Capacitor アプリにはデータを保存する場所がいくつかあります。それぞれが異なる問題を解決します:
| ストレージ | 適した用途 | 制限事項 |
|---|---|---|
localStorage / IndexedDB |
使い捨てキャッシュ | WebView 内に存在し、OS によって削除される可能性があります。localStorage では本格的なクエリは実行できません |
@capacitor/preferences |
小さな設定数項目(テーマ、言語など) | 文字列値のみ。大容量データには不向きで、暗号化もされていません |
| Keychain / Keystore ストレージ | トークン、暗号化キー | 少量のシークレットのみ |
ファイル(@capacitor/filesystem) |
画像、PDF、エクスポート | クエリなし |
| SQLite | レコード、オフラインデータ、同期キュー、検索 | スキーマとマイグレーションを管理 |
オフラインファーストなデータ、数百件を超えるレコード、フィルタリングやソート、テーブル間のリレーション、アトミックな複数ステップの書き込み、または構造化データの保存時暗号化が必要な場合は、SQLiteを選択してください。
2026年にCapacitor SQLiteプラグインを選ぶ
Capacitor 8でネイティブSQLiteを利用するための、現実的な選択肢は3つあります。
@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 チャネル、ブリッジをバイパス | Capacitor ブリッジ | Capacitor ブリッジ |
| 暗号化 | 任意の SQLCipher(iOS、Android) | SQLCipher は常にバンドル | SQLCipher、設定にパスフレーズ |
| 行フォーマット | カラムをキーとするオブジェクト | カラムをキーとするオブジェクト | 文字列 |
| ORMサポート | カスタムドライバー (Kysely, Drizzle) | TypeORM 内蔵ドライバー | なし |
このガイドでは Fast SQL を使用します。Fast SQL は Capgo によってメンテナンスされ、Capacitor のメジャーバージョン (Capacitor 8 向けの plugin v8) に追従しており、SQLCipher はオプトインのまま、ブリッジ経由で大きな結果セットをシリアライズすることも避けます。すでにコミュニティプラグインを使用している場合は、こちらの 比較と移行ガイド してください。
Fast SQL のインストール
bun add @capgo/capacitor-fast-sql
bunx cap sync
Fast SQL は、以下のポートにバインドされた小さな HTTP サーバーを起動し、 localhost 接続ごとの bearer token を使って SQL を送信します。両プラットフォームともデフォルトで平文 HTTP をブロックするため、ループバックに対してのみ許可してください。
iOS: ローカルネットワークを許可
これを以下に追加してください: ios/App/App/Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
NSAllowsLocalNetworking これはループバックとローカルアドレスのみが対象です。実際の API 呼び出しに対する App Transport Security の制限を緩和するものではありません。
Android: localhost への cleartext 通信を許可
作成します 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 ブロックを追加してください。2つ目のファイルを作成しないでください。
データベースを一度だけ開く
接続を管理する単一のモジュールを作成します。同じデータベースを繰り返し開くのは無駄であり、マイグレーションを実行する場所を1か所に集約したいからです。
// 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 によるスキーママイグレーション
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}`);
});
}
}
安全性を維持するためのルール:
- 1 つの SQL 文につき 1 つの
execute()呼び出し。ネイティブ実装は文字列の最初の文のみを準備するため、"CREATE ...; CREATE ..."はそれらの 1 つを無言で実行します。 - 各バージョンはトランザクション内で実行されるため、途中でクラッシュしてもデータベースは前のバージョンのままになり、次の起動時にそのステップが再実行されます。
- 追加的な変更(新しいテーブル、新しい NULL 許容カラム)を優先します。SQLite は
ALTER TABLE ... DROP COLUMNとRENAME COLUMNを最近のバージョンでサポートしていますが、古い Android リリースのシステム SQLite は遅れている可能性があるため、テーブルを再構成する移植可能な方法は、新しいテーブルを作成し、コピーし、古いものを削除し、名前を変更することです。
マイグレーションと Live Updates
JavaScript の変更を Capgo Live Updatesで配信する場合、マイグレーションの code もバンドルに同梱されます。スキーマ変更にはストアへのリリースが不要なので便利です。ただし、ロールバック時に新しいスキーマの上に古いバンドルが載る可能性があります。マイグレーションは後方互換性を保ちましょう(カラムの追加は可、使用をやめるリリースでリネームや削除は不可)。そうすれば古いバンドルも動作し続けます。
データの読み書き
SQLConnection には、頻繁に使う4つのメソッドがあります:
| メソッド | 戻り値 | は |
|---|---|---|
query(sql, params) |
SQLRow[] |
SELECT と PRAGMA の読み取りに |
run(sql, params) |
{ rowsAffected, insertId } |
INSERT, UPDATE, DELETE に |
execute(sql, params) |
{ rows, rowsAffected, insertId } |
何でも可、完全な結果を返す |
executeBatch(ops) |
SQLResult[] |
1回の往復で複数のステートメントを実行 |
常に値を ? プレースホルダーにバインドしてください。ユーザー入力から文字列連結で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にはboolean型がないため、 1 と 0 を保存し、データレイヤーの境界で setPinned が判断基準です。
トランザクションとバッチ書き込み
すべての複数ステップの書き込みをトランザクションでラップしてください。データの整合性を保ち、SQLiteがコミットごとに1回だけディスクに同期するため、文ごとに1回同期する場合よりもはるかに高速です。
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() は、コールバックが解決したときにコミットし、例外がスローされたときにロールバックします。手動制御が必要な場合は、 beginTransaction(), commit() と rollback() 接続について。
すべての呼び出しは、データベースごとに単一のネイティブ接続を共有します。トランザクションが実行されている間、同じデータベースに書き込む他の code もその中で実行されます。トランザクションは短く保ち、その中でネットワークリクエストの await を避けてください。
バイナリデータの保存
次を渡すと、 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 をサポートしています。これはオプトイン形式なので、必要のないアプリに同梱されることはありません:
const db = await FastSQL.connect({
database: 'vault',
encrypted: true,
encryptionKey: keyFromSecureStorage,
});
Android の場合は、 net.zetetic:sqlcipher-android をアプリの build.gradleに追加します。iOS の場合は、pod を CapgoCapacitorFastSql/SQLCipher キーの生成、Keychain や Keystore への保存、既存のプレーンテキストデータベースの暗号化、キーのローテーションについては、次の記事でステップごとに解説しています: Capacitor で SQLite データベースを暗号化する方法Web では暗号化は利用できません。
Web対応
Web では Fast SQL は公式の @sqlite.org/sqlite-wasm ビルドを使用して、ファイルを Origin Private File System (OPFS) に保存します。これはメモリ上に保持して IndexedDB に書き出すコピーではなく、本物の SQLite ファイルです。
OPFS には cross-origin isolation が必要なため、開発サーバーと本番ホストの両方が次の2つのヘッダーを送信する必要があります:
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'],
},
});
ヘッダーが不足している場合、プラグインは警告をログに出力し、非永続的なデータベースにフォールバックします。この場合、リロードするまでは正常に動作します。 CapgoCapacitorFastSql.configureWeb({ useOpfs, worker }) 初回実行の前にデフォルトを変更できます。 connect()また、ネイティブ環境では no-op です。
require-corp また、CORP や CORS ヘッダーを送信しないクロスオリジンの画像やスクリプトもブロックするため、有効にする際はサードパーティの埋め込みをテストしてください。
データベースファイルの保存場所
| プラットフォーム | 位置情報 | 備考 |
|---|---|---|
| iOSと | Documents/<name>.db |
デフォルトで iCloud およびデバイスバックアップに含められます |
| Androidでは | files/<name>.db アプリの内部ストレージに |
Android の自動バックアップルールが適用されます |
| Web | ページオリジン用の OPFS | ユーザーがサイトデータをクリアすると削除されます |
これには2つの実用上の影響があります。まず、アプリをアンインストールすると、両方のモバイルプラットフォームでファイルが削除されます。次に、バックアップによって新しいデバイスにデータベースファイルを復元できます。これは通常望ましい動作ですが、バックアップに含まれないキーでデータベースが暗号化されている場合、復元されたファイルを開けません。その対処法は暗号化ガイドで説明しています。
パフォーマンスチェックリスト
- オンにする
walMode。リーダーがライターをブロックしなくなります。 - で書き込みをトランザクションにまとめる
executeBatch。インポートや同期において最大の効果を発揮します。 - フィルタやソートに使うカラムにインデックスを追加し、その後で確認する
EXPLAIN QUERY PLAN. - 必要なカラムだけを選択します。大きな
TEXTとBLOBカラムは、HTTPチャネル経由でも転送時間がかかります。 - でページネーションする
WHERE created_at < ? ORDER BY created_at DESC LIMIT 50大きな〜の代わりにOFFSET値。 - 大量のデータを削除した後、実行してください
VACUUMファイルサイズを縮小するために定期的に実行します。データベース全体を書き直すため、アイドル時間に実行してください。
トラブルシューティング
SQL execution failed iOSでは、connect直後に実行します。 ローカルHTTPリクエストがブロックされました。 NSAllowsLocalNetworking が正しい Info.plist であることを確認し、ネイティブアプリを再ビルドしてください。ライブリロードでは不十分です。
CLEARTEXT communication to localhost not permitted Androidの場合 ネットワークセキュリティ設定が欠落しているか、マニフェストから参照されていません。
最初のステートメントのみ実行されました。 1つの文字列に複数のステートメントを渡しました。分割するか、 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で問題ありません。テーブルが数個を超えたら、型付きレイヤーが役立ちます:
- KyselyとCapacitor、SQLite:Fast SQL 用の小さなカスタムドライバーを備えた型付きクエリビルダー。
- Drizzle ORM と Capacitor、SQLite:TypeScript でのスキーマ定義、自動生成されたマイグレーション。
- TypeORM と Capacitor、SQLite:デコレーターとリポジトリ。
- Capacitor でのキーバリューストレージの選択肢:Preferences または SQLite の KV テーブルで十分な場合。
プラグインの参照は Fast SQL ドキュメント および プラグインのページにあります。Fast SQL を追加したり設定を変更したりすると、ネイティブの再ビルドが必要です。そのために Mac を維持したくない場合は、 Capgo のビルド クラウド上で署名済みの iOS および Android バイナリを生成できます。2026 年 4 月以降、App Store へのアップロードには Xcode 26 以降でのビルドが必須となるため、次のリリース前にビルド環境を確認してください。