Per usare SQLite in un'app Capacitor, installa un plugin SQLite nativo come ad esempio @capgo/capacitor-fast-sql, open a database once at startup, create your schema with versioned migrations, then read and write with parameterized SQL. This guide walks through that full setup on Capacitor 8 for iOS, Android and the web, including transactions, encryption, file locations, and the errors you are most likely to hit.
Se hai bisogno solo di poche impostazioni, SQLite è un eccesso. Se memorizzi liste, registri o qualsiasi cosa vuoi filtrare, ordinare o sincronizzare, è lo strumento giusto.
Quando SQLite è la scelta di archiviazione giusta
Capacitor le applicazioni hanno diversi luoghi per memorizzare i dati. Risolvono problemi diversi:
| Archiviazione | Adatto per | Limiti |
|---|---|---|
localStorage / IndexedDB |
Cache temporaneo | Vive dentro il WebView, il sistema operativo può espellerlo, nessun vero query in localStorage |
@capacitor/preferences |
Un paio di piccole impostazioni (tema, lingua) | Valori di stringa solo, non destinati a grandi dati, non crittografati |
| Memorizzazione della chiave / Keystore | Token, chiavi di cifratura | Piccoli segreti solo |
File ("@capacitor/filesystem) |
immagini, PDF, esportazioni | Nessuna query |
| SQLite | Registri, dati offline, code di sincronizzazione, ricerche | Gestisci schema e migrazioni |
Scegli SQLite quando hai bisogno di uno dei seguenti: dati offline, più di poche centinaia di record, filtri e ordinamento, relazioni tra tabelle, scritture atomiche multi-step, o crittografia dei dati strutturati in stato di riposo.
Scegliere un plugin SQLite Capacitor nel 2026
Ci sono tre opzioni realistiche per SQLite nativo su Capacitor 8:
@capgo/capacitor-fast-sql |
@capacitor-community/sqlite |
@capgo/capacitor-data-storage-sqlite |
|
|---|---|---|---|
| Funzione | Database SQL generale | Database SQL generale | Archivio di chiavi-valori in formato SQLite |
| Piattaforme | iOS, Android, Web | iOS, Android, Web, Electron | iOS, Android, Web, Electron |
| Motore web | SQLite Wasm ufficiale + OPFS | jeep-sqlite (sql.js + IndexedDB) | IndexedDB |
| Trasporto dei dati | Canale HTTP locale, bypassa il ponte | Capacitor ponte | Capacitor ponte |
| Crittografia | SQLCipher facoltativo (iOS, Android) | SQLCipher sempre integrato | SQLCipher, frase d'accesso nella configurazione |
| Formato riga | Oggetti chiavi da colonna | Oggetti chiavi da colonna | Stringhe |
| Supporto ORM | Driver personalizzati (Kysely, Drizzle) | Driver integrato TypeORM | Nessuno |
Questa guida utilizza Fast SQL. È mantenuto da Capgo, segue Capacitor versioni principali (plugin v8 per Capacitor 8), mantiene SQLCipher a opzione, e evita la serializzazione dei grandi set di risultati attraverso il ponte. Se già esegui il plugin della community, leggi la nostra guida di confronto e migrazione. Guida di confronto e migrazione Primo.
Installa Fast SQL
bun add @capgo/capacitor-fast-sql
bunx cap sync
Fast SQL avvia un piccolo server HTTP legato a localhost e invia SQL a esso con un token di accesso per connessione. Entrambe le piattaforme bloccano HTTP chiaro per impostazione predefinita, quindi consentirelo solo per loopback.
iOS: consenti la rete locale
Aggiungi questo ios/App/App/Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
NSAllowsLocalNetworking Solo copre indirizzi di loopback e locali. Non rilassa la sicurezza dei trasporti dell'applicazione per le tue chiamate reali API.
Android: consentire il testo chiaro a localhost
Crea 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>
Riferiscilo in android/app/src/main/AndroidManifest.xml:
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
Se già hai una configurazione di sicurezza della rete (ad esempio per il pinning SSL), aggiungi il domain-config crea un blocco in esso anziché creare un secondo file.
Apri la database una volta
Crea un modulo singolo che gestisce la connessione. Aprire più volte la stessa database è inutile, e desideri un solo punto per eseguire le migrazioni.
// 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 and performancePresets sono disponibili a partire dalla versione del plugin 8.0.49. performancePresets anche attiva foreign_keys, che SQLite lascia fuori di default, quindi ON DELETE CASCADE funziona effettivamente.
Chiama getDb() ovunque avrai bisogno del database. La prima chiamata apre e migra, le chiamate successive riutilizzano la stessa promessa. Se l'apertura o la migrazione fallisce, la chiamata successiva riprova.
Schemi di migrazione con PRAGMA user_version
Fast SQL ti offre SQL raw, non un framework di migrazione, il che è sufficiente perché SQLite ha già un slot di versione nel file header: PRAGMA user_versionConserva un array ordinato di passaggi e applica solo ciò che manca.
// 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}`);
});
}
}
Le regole che mantengono questo al sicuro:
- Una sola istruzione SQL per
execute()chiamata. Entrambi gli implementazioni native preparano solo la prima istruzione di una stringa, quindi"CREATE ...; CREATE ..."esegue silenziosamente una di esse. - Ogni versione esegue in una transazione, quindi un crash a metà lascia il database alla versione precedente e il passaggio si ripete alla prossima esecuzione.
- Preferisci modifiche additive (nuove tabelle, nuove colonne nullable). SQLite supporta
ALTER TABLE ... DROP COLUMNeRENAME COLUMNIn versioni recenti, ma il sistema SQLite nelle rilasci Android più vecchi potrebbe essere indietro, quindi il modo portatile per ricreare una tabella è creare una nuova tabella, copiare, eliminare quella vecchia, rinominare.
Migrazioni e aggiornamenti in tempo reale
Se invii cambiamenti JavaScript con Capgo aggiornamenti in tempo realeSe invii cambiamenti JavaScript con code aggiornamenti in tempo reale, la tua migrazione code parte con il pacchetto. È comodo, poiché un cambiamento di schema non richiede una versione del magazzino. Ciò significa anche che un rollback può mettere una versione più vecchia sopra uno schema più nuovo. Mantieni le migrazioni compatibili con le versioni precedenti (aggiungi colonne, non rinomina o elimina quelle che non vengono più utilizzate nello stesso rilascio) e una versione più vecchia continua a funzionare.
La lettura e la scrittura dei dati
SQLConnection ti dà quattro metodi che utilizzerai sempre:
| Metodo | Restituisce | Utilizza per |
|---|---|---|
query(sql, params) |
SQLRow[] |
SELECT e PRAGMA letture |
run(sql, params) |
{ rowsAffected, insertId } |
INSERT, UPDATE, DELETE |
execute(sql, params) |
{ rows, rowsAffected, insertId } |
Qualsiasi cosa, risultato completo |
executeBatch(ops) |
SQLResult[] |
Molti comandi in un viaggio di andata e ritorno |
Lega sempre i valori con ? placeholder. Non costruire SQL con la concatenazione di stringhe da input dell'utente.
// 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;
}
Le righe tornano come oggetti semplici chiave-valore con nome di colonna, quindi non è necessario un passaggio di mappatura colonne/valori. SQLite non ha un tipo booleano, quindi memorizza 1 e 0 e converte al bordo del tuo layer dati, come setPinned fa.
Operazioni di transazione e scritture in batch
Avvolgi ogni scrittura a più passaggi in una transazione. Mantiene i dati coerenti e è molto più veloce, perché SQLite sincronizza con il disco una sola volta per commit anziché una volta per comando.
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() commette quando la callback risolve e annulla se si lancia. Se hai bisogno di un controllo manuale c'è anche beginTransaction(), commit() e rollback() sulla connessione.
Tutte le chiamate condividono una connessione nativa unica per database. Mentre una transazione è aperta, altre code che scrivono nello stesso database eseguono all'interno di essa. Mantieni le transazioni brevi e evita di attendere richieste di rete all'interno di esse.
Memorizzare dati binari
Passa un Uint8Array e ottieni un Uint8Array indietro. Il plugin codifica in base64 per il trasporto e memorizza un vero 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;
Mantieni grandi media (foto, video) come file e memorizza solo la percorso in SQLite. I BLOB sono adatti per miniature, piccoli allegati e embedding.
Criptare il database
SQL veloce supporta SQLCipher su iOS e Android. È opzionale, quindi gli app che non ne hanno bisogno non lo caricano:
const db = await FastSQL.connect({
database: 'vault',
encrypted: true,
encryptionKey: keyFromSecureStorage,
});
Su Android aggiungi net.zetetic:sqlcipher-android alla tua app's build.gradleOn iOS cambia il pod per il CapgoCapacitorFastSql/SQLCipher sottospecifica. La generazione della chiave, la memorizzazione nella Keychain o Keystore, l'encryptazione di un database di testo esistente e la rotazione delle chiavi sono trattate passo dopo passo in Come criptare un database SQLite in CapacitorLa crittografia non è disponibile sul web.
Supporto web
Sul web Fast SQL utilizza la versione ufficiale @sqlite.org/sqlite-wasm e memorizza il file nel Sistema di File Privati di Origine (OPFS). È un file SQLite reale, non una copia tenuta in memoria e scritta su IndexedDB.
Il tuo server di sviluppo e il tuo host di produzione devono inviare due intestazioni:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Con 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'],
},
});
Se le intestazioni mancano, il plugin registra un avviso e ricade su un database non persistente: tutto funziona fino a quando non si ricarica. CapgoCapacitorFastSql.configureWeb({ useOpfs, worker }) ti consente di modificare i valori di default prima della prima connect()e è un'operazione senza effetti sul nativo.
require-corp anche blocca immagini e script di origine diversa che non inviano i header CORP o CORS, quindi testa gli embed di terze parti quando lo attivi.
Dove risiede il file del database
| Piattaforma | Posizione | Note |
|---|---|---|
| iOS | Documents/<name>.db |
Incluso di default nei backup iCloud e dispositivi |
| Android | files/<name>.db Nel storage interno dell'applicazione |
Sottoposto alle regole di backup Android Auto |
| Web | OPFS per l'origine della pagina | Pulito se l'utente cancella i dati del sito |
Due a due conseguenze pratiche. Prima, disinstallando l'app elimina il file su entrambe le piattaforme mobili. Secondo, i backup possono ripristinare il file del database su un nuovo dispositivo. È di solito ciò che desideri, ma se il database è crittografato con una chiave che non viaggia con il backup, il file ripristinato non può essere aperto. La guida sulla crittografia copre come gestire questo aspetto.
Elenco di controllo di prestazioni
- Attiva
walModeLeggitori non bloccano l'autore. - Scrivi in batch in una transazione con
executeBatchQuesto è il più grande vantaggio per gli import e la sincronizzazione. - Aggiungi gli indici per le colonne che filtri o ordini, quindi controlla con
EXPLAIN QUERY PLAN. - Scegli solo le colonne che ti servono. Grandi
TEXTeBLOBLe colonne costano tempo di trasferimento anche attraverso il canale HTTP. - Pagina con
WHERE created_at < ? ORDER BY created_at DESC LIMIT 50al posto di grandiOFFSETvalori. - Dopo aver eliminato molto dati, esegui
VACUUMoccasionalmente per ridurre le dimensioni del file. Riscrive l'intero database, quindi eseguilo durante il tempo di inattività.
Risolvere i problemi
SQL execution failed dopo la connessione su iOS. su iOS subito dopo la connessione. NSAllowsLocalNetworking La richiesta HTTP locale è stata bloccata. Verifica che Info.plist e ricostruisci l'app nativa, una ricarica in tempo reale non è sufficiente.
CLEARTEXT communication to localhost not permitted su Android. La configurazione di sicurezza della rete manca o non è stata riferita dal manifesto.
Solo la prima dichiarazione è stata eseguita. Hai passato più di una dichiarazione in una stringa. Scomponile, o utilizza executeBatch.
no such table Dopo un aggiornamento. La migrazione non è stata eseguita, o è stata eseguita su un altro nome di database. Registra PRAGMA user_version all'avvio e confronta con il tuo array di passaggi.
Un filtro su un valore booleano o calcolato non corrisponde mai su Android. Per le query di lettura l'implementazione Android lega i parametri come testo. Le comparazioni contro una colonna con affinità INTEGER funzionano ancora perché SQLite converte il testo, ma le comparazioni contro espressioni (ad esempio json_extract(...) = ? o count(*) > ?) confronta il testo con un numero. Passa 1/0 oppure true/falsepassa CAST(? AS INTEGER) quando si confronta con un'espressione.
INSERT ... RETURNING non restituisce righe su Android. Nel corrente plugin versione, Android restituisce solo righe per le dichiarazioni che iniziano con SELECT, PRAGMA o EXPLAIN. Utilizza insertId da run() e seleziona la riga successivamente. Lo stesso vale per le query che iniziano con WITHavvolgile in modo che inizino con SELECT, o testarle su Android in anticipo.
I dati scompaiono alla ricarica del browser. OPFS non è disponibile, quasi sempre a causa degli header COOP/COEP mancanti sul host di distribuzione.
Encryption is not available in this build su iOS. Sei passato encrypted: true senza dover passare alla subspec SQLCipher.
Andare oltre
La SQL cruda va bene per le piccole app. Una volta che hai più di poche tabelle, un layer tipizzato aiuta:
- Kysely con Capacitor e SQLite: costruttore di query tipizzato con un piccolo driver personalizzato per Fast SQL.
- Drizzle ORM con Capacitor e SQLite: schema in TypeScript, migrazioni generate.
- TypeORM con Capacitor e SQLite: decoratori e repository.
- Opzioni di archiviazione a valore chiave in Capacitor: quando le Preferenze o una tabella KV su SQLite sono sufficienti.
La documentazione del plugin si trova nella Risorse SQL context pagina del pluginFast SQL richiede una ricostruzione nativa quando lo aggiungi o modifichi la sua configurazione. Se non vuoi mantenere un Mac per questo scopo, Capgo Costruisci puoi produrre binari iOS e Android firmati nel cloud. Dalla primavera 2026 gli upload di App Store devono essere costruiti con Xcode 26 o successivi, quindi controlla la tua macchina di costruzione prima della prossima release.