メインコンテンツへスキップ

Capacitor アプリで SQLite を使う方法(2026年版ガイド)

Capacitor アプリで SQLite を使う方法を解説:プラグインの選び方、データベースのオープン、マイグレーション、トランザクション、暗号化、Web サポート、そして動作する code まで。

Capacitor アプリで SQLite を使うガイドの Capacitor ロゴ

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で問題ありません。テーブルが数個を超えたら、型付きレイヤーが役立ちます:

プラグインの参照は Fast SQL ドキュメント および プラグインのページにあります。Fast SQL を追加したり設定を変更したりすると、ネイティブの再ビルドが必要です。そのために Mac を維持したくない場合は、 Capgo のビルド クラウド上で署名済みの iOS および Android バイナリを生成できます。2026 年 4 月以降、App Store へのアップロードには Xcode 26 以降でのビルドが必須となるため、次のリリース前にビルド環境を確認してください。

Capacitor アプリ向けの Live Updates

Web レイヤーのバグが本番で発生したら、App Store の審査を何日も待たずに Capgo で修正を配信しましょう。ネイティブの変更は通常の審査フローに残したまま、ユーザーはバックグラウンドでアップデートを受け取れます。

最新のブログ記事

Capgoは、真にプロフェッショナルなモバイルアプリを作るために必要な最高のインサイトを提供します。