Untuk menggunakan SQLite di aplikasi Capacitor, instal plugin SQLite native seperti @capgo/capacitor-fast-sql, buka database sekali pada startup, buat schema dengan migrasi versi, kemudian baca dan tulis dengan SQL parameterized. Panduan ini menjelaskan setup lengkap tersebut pada Capacitor 8 untuk iOS, Android, dan web, termasuk transaksi, enkripsi, lokasi file, dan kesalahan yang paling mungkin Anda temui.
Jika Anda hanya membutuhkan beberapa pengaturan, SQLite adalah berlebihan. Jika Anda menyimpan daftar, rekaman, atau apa pun yang ingin Anda filter, urutkan, atau sinkronkan, itu adalah alat yang tepat.
Ketika SQLite adalah pilihan penyimpanan yang tepat
Aplikasi Capacitor memiliki beberapa tempat untuk menyimpan data. Mereka menyelesaikan masalah yang berbeda:
| Penyimpanan | Baik untuk | Batasan |
|---|---|---|
localStorage / IndexedDB |
Cache sementara | Berada di dalam WebView, sistem operasi dapat mengusirnya, tidak ada kueri nyata di localStorage |
@capacitor/preferences |
Beberapa pengaturan kecil (tema, bahasa) | Nilai string saja, tidak untuk data besar, tidak terenkripsi |
| Penyimpanan Keychain / Keystore | Token, kunci enkripsi | Rahasia kecil saja |
File (@capacitor/filesystem) |
Gambar, PDF, ekspor | Tidak ada querying |
| SQLite | Catatan, data offline, antrian sinkronisasi, pencarian | Manajemen skema dan migrasi |
Pilih SQLite ketika Anda membutuhkan salah satu dari berikut: data offline pertama, lebih dari beberapa ratus catatan, filter dan pengurutan, hubungan antar tabel, penulisan multi-langkah atomik, atau enkripsi pada data struktur untuk data yang ada di rest.
Choosing a Capacitor SQLite plugin in 2026
Ada tiga opsi yang realistis untuk SQLite native pada Capacitor 8:
@capgo/capacitor-fast-sql |
@capacitor-community/sqlite |
@capgo/capacitor-data-storage-sqlite |
|
|---|---|---|---|
| Tujuan | Database SQL umum | Database SQL umum | Penyimpanan nilai kunci-string pada SQLite |
| Platform | iOS, Android, Web | iOS, Android, Web, Electron | iOS, Android, Web, Electron |
| Mesin Web | SQLite Wasm + OPFS Resmi | jeep-sqlite (sql.js + IndexedDB) | IndexedDB |
| Transportasi Data | Saluran HTTP Lokal, menghindari jembatan | Capacitor jembatan | Capacitor jembatan |
| Enkripsi | SQLCipher Opsional (iOS, Android) | SQLCipher selalu dibundel | SQLCipher, sandi dalam konfigurasi |
| Format Baris | Benda-benda yang dikunci oleh kolom | Benda-benda yang dikunci oleh kolom | String |
| Support ORM | Pengemudi kustom (Kysely, Drizzle) | Pengemudi TypeORM bawaan | Tidak ada |
Guid ini menggunakan Fast SQL. Dibuat oleh Capgo, mengikuti Capacitor versi utama (plugin v8 untuk Capacitor 8), menjaga SQLCipher sebagai pilihan, dan menghindari serialisasi hasil set besar melalui bridge. Jika Anda sudah menjalankan plugin komunitas, baca panduan perbandingan dan migrasi kami bandingan dan panduan migrasi kami pertama.
Instal Fast SQL
bun add @capgo/capacitor-fast-sql
bunx cap sync
Fast SQL memulai sebuah server HTTP kecil yang terikat ke localhost dan mengirimkan SQL kepadanya dengan token bearer per-koneksi. Kedua platform ini melarang HTTP jelas oleh default, jadi izinkanlah hanya untuk loopback saja.
iOS: izinkan jaringan lokal
Tambahkan ini ke ios/App/App/Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
NSAllowsLocalNetworking hanya mencakup loopback dan alamat lokal. Ini tidak menghilangkan App Transport Security untuk panggilan API nyata Anda.
Android: izinkan teks jelas ke localhost
Buat 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>
Referensikan android/app/src/main/AndroidManifest.xml:
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
Jika Anda sudah memiliki konfigurasi keamanan jaringan (untuk SSL pinning, misalnya), tambahkan domain-config Blok untuk itu daripada membuat file kedua.
Buka database sekali
Buat modul tunggal yang mengelola koneksi. Membuka database yang sama secara berulang-ulang adalah tidak efisien, dan Anda ingin satu tempat untuk menjalankan migrasi.
// 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 dan performancePresets tersedia dari versi plugin 8.0.49. performancePresets juga mengaktifkan foreign_keys, yang SQLite tinggalkan secara default, sehingga ON DELETE CASCADE benar-benar berfungsi.
Panggil getDb() di mana saja Anda membutuhkan database. Panggilan pertama membuka dan melakukan migrasi, panggilan selanjutnya menggunakan janji yang sama. Jika membuka atau melakukan migrasi gagal, panggilan selanjutnya mencoba lagi.
Migrasi skema dengan PRAGMA user_version
SQL Cepat memberikan Anda SQL mentah, bukan kerangka migrasi, yang tidak masalah karena SQLite sudah memiliki slot versi di header file: PRAGMA user_versionSimpan array langkah yang terurut dan terapkan hanya apa yang hilang.
// 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}`);
});
}
}
Aturan yang menjaga ini aman:
- Satu statement SQL per
execute()Panggilan. Implementasi native hanya mempersiapkan statement SQL pertama dari sebuah string, sehingga"CREATE ...; CREATE ..."langsung menjalankan salah satu dari mereka. - Setiap versi berjalan dalam transaksi, sehingga kegagalan di tengah meninggalkan database pada versi sebelumnya dan langkah berulang kembali pada peluncuran berikutnya.
- Ubah-ubah yang menambahkan (membuat tabel baru, menambahkan kolom nullable baru). SQLite mendukung
ALTER TABLE ... DROP COLUMNdanRENAME COLUMNdalam versi terbaru, tetapi sistem SQLite pada rilis Android yang lebih tua mungkin ketinggalan, sehingga cara yang lebih portable untuk mengubah struktur tabel adalah membuat tabel baru, menyalin, menghapus tabel lama, dan menggantinya.
Migrasi dan pembaruan live
Jika Anda mengirimkan perubahan JavaScript dengan Capgo pembaruan live, migrasi code akan dikirimkan bersama dengan bundle. Hal ini sangat menguntungkan, karena perubahan schema tidak memerlukan rilis penyimpanan. Namun, hal ini juga berarti bahwa rollback dapat mengembalikan bundle yang lebih tua di atas schema yang lebih baru. Pastikan migrasi kompatibel ke belakang (tambahkan kolom, jangan rename atau dropnya dalam rilis yang sama yang menghentikan penggunaannya) dan bundle yang lebih tua masih dapat berjalan.
Membaca dan menulis data
SQLConnection memberikan empat metode yang akan Anda gunakan secara terus-menerus:
| Metode | Mengembalikan | Gunakan untuk |
|---|---|---|
query(sql, params) |
SQLRow[] |
Pemilihan dan baca PRAGMA |
run(sql, params) |
{ rowsAffected, insertId } |
INSERT, UPDATE, DELETE |
execute(sql, params) |
{ rows, rowsAffected, insertId } |
Apapun, hasil penuh |
executeBatch(ops) |
SQLResult[] |
Banyak statement dalam satu perjalanan |
Selalu ikat nilai dengan ? tempat pengganti. Tidak pernah bangun SQL dengan penggabungan string dari input pengguna.
// 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;
}
Sekarang datang sebagai objek sederhana yang dikunci oleh nama kolom, sehingga tidak ada langkah pemetaan kolom/nilai. SQLite tidak memiliki jenis boolean, sehingga simpan 1 dan 0 dan konversi di tepi lapisan data Anda, seperti setPinned seperti itu.
Transaksi dan penulisan batch
Selubungi setiap penulisan multi-langkah dalam transaksi. Ini menjaga konsistensi data dan sangat lebih cepat, karena SQLite sinkron ke disk sekali per komit daripada sekali per statement.
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() transaksi akan ditransaksikan ketika callback diselesaikan dan dibatalkan jika terjadi kesalahan. Jika Anda memerlukan kontrol manual, ada juga beginTransaction(), commit() dan rollback() pada koneksi.
Semua panggilan menggunakan koneksi native tunggal per database. Sementara transaksi terbuka, lainnya code yang menulis ke database yang sama berjalan di dalamnya. Jaga transaksi singkat dan hindari menunggu permintaan jaringan di dalamnya.
Menyimpan data biner
Pass sebuah Uint8Array dan Anda mendapatkan sebuah Uint8Array Kembali. Plugin mengenkripsi base64-nya untuk transportasi dan menyimpan BLOB yang sebenarnya.
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;
Tetapkan media besar (foto, video) sebagai file dan simpan hanya jalur di SQLite. BLOBs cocok untuk miniatur, lampiran kecil, dan penggabungan.
Enkripsi basis data
SQLCipher didukung di iOS dan Android. Ini adalah fitur yang dapat diaktifkan, sehingga aplikasi yang tidak membutuhkannya tidak akan mengirimkannya.
const db = await FastSQL.connect({
database: 'vault',
encrypted: true,
encryptionKey: keyFromSecureStorage,
});
Pada Android Anda tambahkan net.zetetic:sqlcipher-android ke aplikasi Anda's build.gradle. Pada iOS Anda beralih ke pod ke CapgoCapacitorFastSql/SQLCipher subspec. Membuat kunci, menyimpannya di Keychain atau Keystore, mengenkripsi basis data plaintext yang ada dan memutar kunci dijelaskan secara langkah demi langkah di Bagaimana mengenkripsi basis data SQLite di Capacitor. Enkripsi tidak tersedia di web.
Bantuan Web
Pada web SQL Cepat menggunakan resmi @sqlite.org/sqlite-wasm membangun dan menyimpan file di Sistem File Pribadi Asal (OPFS). Ini adalah file SQLite yang sebenarnya, bukan salinan yang disimpan di memori dan dihapus ke IndexedDB.
OPFS memerlukan isolasi asal-usul yang berbeda, jadi server pengembang dan host produksi Anda harus mengirim dua header:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Dengan 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'],
},
});
Jika header tersebut hilang, plugin mencetak peringatan dan kembali ke basis data yang tidak berlangsung: segalanya berfungsi sampai Anda reload. CapgoCapacitorFastSql.configureWeb({ useOpfs, worker }) memungkinkan Anda mengubah default sebelum pertama kali connect()dan itu adalah operasi tidak berpengaruh pada native.
require-corp juga menghalangi gambar dan skrip asal-usul yang berbeda yang tidak mengirimkan header CORP atau CORS, sehingga uji embed pihak ketiga ketika Anda mengaktifkannya.
Dimana file basis data berada
| Platform | Lokasi | Catatan |
|---|---|---|
| iOS | Documents/<name>.db |
Termasuk dalam cadangan iCloud dan perangkat secara default |
| Android | files/<name>.db dalam penyimpanan internal aplikasi |
Tergantung pada aturan Android Auto Backup |
| Web | OPFS untuk asal halaman | Dihapus jika pengguna menghapus data situs |
Dua konsekuensi praktis. Pertama, menghapus aplikasi menghapus file pada kedua platform mobile. Kedua, cadangan dapat memulihkan file basis data ke perangkat baru. Itu biasanya apa yang Anda inginkan, tetapi jika basis data dienkripsi dengan kunci yang tidak berjalan bersama cadangan, file yang dipulihkan tidak dapat dibuka. Panduan enkripsi menjelaskan cara mengatasi hal itu.
Daftar periksa kinerja
- Mengaktifkan
walModePembaca berhenti menghalangi penulis. - Mengirimkan tulisan dalam transaksi dengan
executeBatch. Ini adalah kemenangan terbesar untuk import dan sinkronisasi. - Tambahkan indeks untuk kolom yang Anda filter atau sortir, kemudian cek dengan
EXPLAIN QUERY PLAN. - Pilih hanya kolom yang Anda butuhkan. Besar
TEXTdanBLOBkolom bahkan biaya waktu transfer bahkan melalui saluran HTTP. - Paginasi dengan
WHERE created_at < ? ORDER BY created_at DESC LIMIT 50alih daripada besarOFFSETnilai. - Setelah menghapus banyak data, jalankan
VACUUMsecara berkala untuk mengurangi ukuran file. Ini menulis ulang seluruh database, jadi lakukan saat waktu senggang.
Troubleshooting
SQL execution failed di iOS setelah terhubung. Permintaan HTTP lokal diblokir. Periksa bahwa NSAllowsLocalNetworking ada di tempat yang tepat Info.plist dan bangun kembali aplikasi asli, reload hidup tidak cukup.
CLEARTEXT communication to localhost not permitted di Android. Konfigurasi keamanan jaringan tidak ada atau tidak disebutkan dari manifest.
Hanya statement pertama yang dieksekusi. Anda melewatkan beberapa statement dalam satu string. Pisahkan mereka, atau gunakan executeBatch.
no such table setelah pembaruan. Pemigrasi tidak berjalan, atau berjalan melawan nama basis data lain. Log PRAGMA user_version di awal dan bandingkan dengan array langkah Anda.
Filter pada nilai boolean atau nilai yang dihitung tidak pernah cocok di Android. Untuk kueri baca implementasi Android memasang parameter sebagai teks. Perbandingan terhadap kolom dengan afinitas INTEGER masih berfungsi karena SQLite mengonversi teks, tetapi perbandingan terhadap ekspresi (misalnya json_extract(...) = ? atau count(*) > ?bandingkan teks dengan angka. Lanjutkan 1/0 sebaliknya dari true/false, dan gunakan CAST(? AS INTEGER) ketika membandingkan terhadap ekspresi.
INSERT ... RETURNING tidak mengembalikan baris pada Android. Dalam versi plugin saat ini, Android hanya mengembalikan baris untuk statement yang dimulai dengan SELECT, PRAGMA atau EXPLAINGunakan insertId dari run() dan pilih baris setelahnya. Hal yang sama berlaku untuk kueri yang dimulai dengan WITHtutup mereka dengan cara agar dimulai dengan SELECTatau tes mereka di Android lebih dulu.
Data menghilang ketika di-reload di browser. OPFS tidak tersedia, hampir selalu karena header COOP/COEP hilang di host yang di-deploy.
Encryption is not available in this build di iOS. Anda lulus encrypted: true tanpa harus beralih ke subspec SQLCipher.
Melanjutkan
SQL mentah cocok untuk aplikasi kecil. Ketika Anda memiliki lebih dari beberapa tabel, lapisan yang ditipekan membantu:
- Kysely dengan Capacitor dan SQLite: pembuat query yang ditipekan dengan driver kecil untuk Fast SQL.
- Drizzle ORM dengan Capacitor dan SQLite: skema di TypeScript, migrasi yang dihasilkan.
- TypeORM dengan Capacitor dan SQLite: dekorator dan repositori.
- Opsi penyimpanan nilai kunci di Capacitor: ketika Preferensi atau tabel KV di SQLite sudah cukup.
Referensi plugin hidup di Dokumen SQL cepat dan halaman plugin. Fast SQL needs a native rebuild when you add it or change its configuration. If you do not want to maintain a Mac for that, Capgo Build can produce signed iOS and Android binaries in the cloud. Since April 2026 App Store uploads must be built with Xcode 26 or later, so check your build machine before the next release.