TypeORM funziona in un'app Capacitor tramite la sua integrazione integrata capacitor driver, che esegue ogni query su @capacitor-community/sqliteConfiguri un DataSource With il plugin’s SQLiteConnectionDefinisci gli entità con i decoratori e utilizza i repository e le transazioni come faresti su un server. Questa guida copre la configurazione completa su TypeORM 1.x e Capacitor 8, inclusi le due cose che la maggior parte dei tutorial trascura: i tipi di colonne esplicite per Vite, e come generare le migrazioni anche se il CLI non può parlare con un telefono.
Quando TypeORM è una buona scelta
TypeORM è un ORM di mappatura dei dati con entità, relazioni, repository e un sistema di migrazione. Su mobile fa senso quando:
- hai già utilizzato TypeORM sul backend e vuoi lo stesso stile di entità nell'app
- il tuo modello di dati ha molte relazioni e vuoi
relations: { posts: true }al posto delle join manuale - Il tuo team preferisce classi e decoratori rispetto agli oggetti di schema.
È più pesante di un costruttore di query. Se il dimensionamento del pacchetto e il tempo di avvio contano di più, o vuoi utilizzare @capgo/capacitor-fast-sqlguarda Kysely o Drizzle al suo posto.
Installa
bun add typeorm @capacitor-community/sqlite
bunx cap sync
Importa TypeORM 1.x reflect-metadata non è necessario un import separato in cima main.ts come mostrano le guide più vecchie. Il plugin della community bundla SQLCipher su iOS e Android, indipendentemente dal fatto che si cripi o meno, il che conta quando si rispondono alle domande di conformità dell'App Store.
Impostazioni TypeScript
{
"compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false
}
}
experimentalDecorators abilita i decoratori legacy di TypeORM. useDefineForClassFields: false ferma gli inizializzatori dei campi di classe da sovrascrivere i valori impostati da TypeORM, il che conta quando il tuo target è ES2022 o successivo.
Entità che funzionano con Vite e esbuild
La maggior parte delle Capacitor app è costruita con Vite, che compila TypeScript con esbuild. esbuild supporta i decoratori ma non li implementa. emitDecoratorMetadatacosì TypeORM non può leggere i tipi di proprietà in esecuzione. Senza tipi espliciti otteni ColumnTypeUndefinedErrorDai un tipo a ogni colonna:
// src/db/entities/author.ts
import {
Column,
CreateDateColumn,
Entity,
OneToMany,
PrimaryGeneratedColumn,
type Relation,
} from 'typeorm';
import { Post } from './post';
@Entity('authors')
export class Author {
@PrimaryGeneratedColumn()
id!: number;
@Column('text')
name!: string;
@Column('text', { unique: true })
email!: string;
@CreateDateColumn()
createdAt!: Date;
@OneToMany(() => Post, (post) => post.author)
posts!: Relation<Post[]>;
}
// src/db/entities/post.ts
import {
Column,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
type Relation,
} from 'typeorm';
import { Author } from './author';
@Entity('posts')
export class Post {
@PrimaryGeneratedColumn()
id!: number;
@Column('text')
title!: string;
@Column('boolean', { default: false })
published!: boolean;
@Index()
@Column('integer')
authorId!: number;
@ManyToOne(() => Author, (author) => author.posts, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'authorId' })
author!: Relation<Author>;
}
Note:
Relation<T>è un wrapper di tipo solo. Evita i problemi di importo circolare traauthor.tsepost.tsse mai compilare con metadati di decoratore (ad esempio nei CLI o nei test).- L'esplicito
authorIdcolonna ti consente di filtrare e inserire per id senza caricare l'autore. - SQLite non ha un tipo booleano. TypeORM memorizza
booleanle colonne come0/1e le converte nuovamente.
Configura la DataSource
// src/db/data-source.ts
import { CapacitorSQLite, SQLiteConnection } from '@capacitor-community/sqlite';
import { DataSource } from 'typeorm';
import { Author } from './entities/author';
import { Post } from './entities/post';
import { Init1790000000000 } from './migrations/1790000000000-Init';
export const sqliteConnection = new SQLiteConnection(CapacitorSQLite);
export const AppDataSource = new DataSource({
type: 'capacitor',
driver: sqliteConnection,
database: 'app',
mode: 'no-encryption',
entities: [Author, Post],
migrations: [Init1790000000000],
synchronize: false,
migrationsRun: false,
journalMode: 'WAL',
logging: ['error'],
});
Opzione per opzione:
driver: il plugin utilizzaSQLiteConnection. Il driver di TypeORM lo utilizza per creare e aprire la connessione, quindi attivaPRAGMA foreign_keyscosìonDelete: 'CASCADE'funziona.database: il plugin aggiunge unSQLite.dbsuffix, quindi il file èappSQLite.db.mode:no-encryption, o uno dei modi di crittografia del plugin (secret,encryption). Vedi crittografare SQLite in Capacitor.synchronize: false: in produzione, sempre. La sincronizzazione può eliminare colonne per adattarsi agli enti, che cancella i dati degli utenti sul dispositivo.migrationsRun: false: la documentazione del plugin richiede questo con il tipo capacitor. Esegui le migrazioni da solo subito dopoinitialize(), come mostrato di seguito.journalMode: 'WAL': facoltativo. Migliore concorrenza tra letture e scritture.migrations: riferimenti a classi, non un glob. Non ci è alcun scan del file system in un WebView.
Glob patterni come entities: ['src/**/*.entity.ts'] non funzionano nell'app per la stessa ragione. Importa le classi.
Avvia all'avvio
// src/db/index.ts
import { Capacitor } from '@capacitor/core';
import { AppDataSource, sqliteConnection } from './data-source';
let ready: Promise<typeof AppDataSource> | null = null;
export function initDatabase() {
if (!ready) {
ready = (async () => {
if (Capacitor.getPlatform() === 'web') {
// Needs the jeep-sqlite element in the DOM, see "Web support"
await sqliteConnection.initWebStore();
}
// Clean up connections left over from a hot reload
await sqliteConnection.checkConnectionsConsistency();
if (!AppDataSource.isInitialized) {
await AppDataSource.initialize();
}
await AppDataSource.runMigrations({ transaction: 'each' });
if (Capacitor.getPlatform() === 'web') {
await sqliteConnection.saveToStore('app');
}
return AppDataSource;
})();
}
return ready;
}
Chiamare await initDatabase() prima che la tua app visualizzi qualcosa che legge i dati. transaction: 'each' utilizza un proprio trasazione per ogni migrazione, quindi un fallimento lascia le migrazioni precedenti applicate e la migrazione fallita annullata.
Raccolte: inserisci, seleziona, aggiorna, elimina
import { initDatabase } from './db';
import { Author } from './db/entities/author';
import { Post } from './db/entities/post';
const ds = await initDatabase();
const authors = ds.getRepository(Author);
const posts = ds.getRepository(Post);
// Insert
const ada = await authors.save(authors.create({ name: 'Ada', email: 'ada@example.com' }));
await posts.save([
posts.create({ title: 'Draft', authorId: ada.id }),
posts.create({ title: 'Live', authorId: ada.id, published: true }),
]);
// Select
const published = await posts.find({
where: { authorId: ada.id, published: true },
order: { id: 'DESC' },
});
const one = await authors.findOneBy({ email: 'ada@example.com' });
// Load a relation
const withPosts = await authors.findOne({
where: { id: ada.id },
relations: { posts: true },
});
// Update without loading
await posts.update({ authorId: ada.id }, { published: true });
// Delete
await posts.delete({ published: false });
save() inserts or updates depending on whether the primary key is set, and returns the entity with its generated id. For bulk writes where you do not need entities back, insert() è più veloce perché evita le ricerche aggiuntive.
Costruttore di query per qualsiasi cosa non banale
const stats = await ds
.getRepository(Author)
.createQueryBuilder('a')
.leftJoin('a.posts', 'p')
.select('a.id', 'id')
.addSelect('a.name', 'name')
.addSelect('COUNT(p.id)', 'postCount')
.groupBy('a.id')
.getRawMany<{ id: number; name: string; postCount: number }>();
Gli unione e gli aggregati sono dove un ORM ti salva tempo su mobile. Controlla l' SQL generato con .getSql() mentre sviluppi, e aggiungi gli indici per le colonne utilizzate in WHERE e JOIN.
Transazioni
await ds.transaction(async (manager) => {
const grace = await manager.save(Author, { name: 'Grace', email: 'grace@example.com' });
await manager.save(Post, { title: 'Hello', authorId: grace.id });
});
Utilizza il manager passato al callback per ogni query all'interno della transazione. Se il callback lancia un'eccezione, TypeORM annulla la transazione.
Il driver Capacitor utilizza una sola connessione, quindi non esiste isolamento tra la transazione e altre operazioni: una query effettuata attraverso ds o un altro repository mentre la transazione è aperta viene eseguita sulla stessa connessione e viene commitata o annullata con essa. Non iniziare lavori di database non correlati fino a quando la transazione non si è conclusa, mantieni le transazioni brevi e non aspettare una richiesta di rete all'interno di una.
Migrazioni: generale con un DataSource sqljs locale
Il TypeORM CLI si esegue sul tuo computer, non sul telefono, quindi non può ispezionare il database del dispositivo. I documenti del plugin dicono che le migrazioni per il tipo capacitor devono essere scritte a mano. Puoi comunque ottenere le migrazioni generate: TypeORM sqljs utilizza lo stesso dialetto SQLite, quindi produce SQL che il dispositivo esegue invariato.
1. Aggiungi un CLI-solo DataSource
bun add -d sql.js
// typeorm.cli.ts (project root, not bundled into the app)
import { DataSource } from 'typeorm';
import { Author } from './src/db/entities/author';
import { Post } from './src/db/entities/post';
export default new DataSource({
type: 'sqljs',
location: '.typeorm/schema.sqlite',
autoSave: true,
entities: [Author, Post],
migrations: ['src/db/migrations/*.ts'],
});
Aggiungi .typeorm/ alla .gitignore. Questo file è una copia di base di “cosa l'schema sembra dopo tutte le migrazioni esistenti”.
2. Aggiorna la database di base, poi diff
mkdir -p .typeorm
bun ./node_modules/typeorm/cli.js migration:run -d ./typeorm.cli.ts
bun ./node_modules/typeorm/cli.js migration:generate -d ./typeorm.cli.ts src/db/migrations/AddTags
Bun esegue il data source e le entità di TypeScript direttamente. Il primo comando applica le migrazioni esistenti alla base di dati di scratch. Il secondo la confronta con le tue entità e scrive una nuova classe di migrazione, ad esempio src/db/migrations/1790000123456-AddTags.ts. Poi aggiungi quella classe all' migrations array in data-source.ts.
Una migrazione iniziale generata assomiglia a questo:
import type { MigrationInterface, QueryRunner } from 'typeorm';
export class Init1790000000000 implements MigrationInterface {
name = 'Init1790000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "authors" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" text NOT NULL, "email" text NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "UQ_authors_email" UNIQUE ("email"))`,
);
await queryRunner.query(
`CREATE TABLE "posts" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" text NOT NULL, "published" boolean NOT NULL DEFAULT (0), "authorId" integer NOT NULL, CONSTRAINT "FK_posts_author" FOREIGN KEY ("authorId") REFERENCES "authors" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`,
);
await queryRunner.query(`CREATE INDEX "IDX_posts_authorId" ON "posts" ("authorId")`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_posts_authorId"`);
await queryRunner.query(`DROP TABLE "posts"`);
await queryRunner.query(`DROP TABLE "authors"`);
}
}
Leggi ogni migrazione generata prima di distribuirla. Per SQLite, TypeORM spesso modifica una tabella creando temporary_<table>, copiando righe, eliminando l'originale e rinominando. Ciò è corretto, ma su una grande tabella richiede tempo su un telefono, quindi eseguilo con uno stato di caricamento.
Scrivere migrazioni a mano
Se scrivono le migrazioni da soli, iniziare ogni statement con un aggettivo maiuscolo. Il capacitor eseguitore di query guarda alla prima parola, in modo case-sensitivo, per decidere come chiamare il plugin: BEGIN, COMMIT, ROLLBACK, CREATE, ALTER e DROP vai a execute, INSERT, UPDATE e DELETE vai a rune tutto il resto va a queryUna parola minuscola create table viene inviata come query, il che non è ciò che si desidera per DDL.
Migrazioni e aggiornamenti in tempo reale
Le classi di migrazione fanno parte del tuo bundle JavaScript. Con Capgo aggiornamenti in tempo reale una modifica dello schema raggiunge gli utenti senza una recensione del negozio. Un rollback può quindi ripristinare le versioni più vecchie code su uno schema più recente, quindi preferire le migrazioni additive (nuove tabelle, nuove colonne nullable) e rimuovere le colonne vecchie in una versione successiva.
Supporto web
La plugin della community funziona sul web attraverso il jeep-sqlite componente web (sql.js persistito in IndexedDB). È utile per lo sviluppo nel browser:
bun add jeep-sqlite
cp node_modules/sql.js/dist/sql-wasm.wasm public/assets/
// main.ts, before initDatabase()
import { defineCustomElements as jeepSqlite } from 'jeep-sqlite/loader';
import { Capacitor } from '@capacitor/core';
if (Capacitor.getPlatform() === 'web') {
jeepSqlite(window);
const el = document.createElement('jeep-sqlite');
document.body.appendChild(el);
await customElements.whenDefined('jeep-sqlite');
}
Nel web il database vive in memoria e viene scritto in IndexedDB quando chiami sqliteConnection.saveToStore('app'). Chiamalo dopo gli scritti che ti interessano, o i dati vengono persi al riavvio.
Troubleshooting
Column type for Post#title is not defined and cannot be guessed. esbuild non ha emesso metadati. Aggiungi un tipo esplicito al @Column decorator.
Capacitor package has not been found installed. The driver L'opzione manca o è indefinita. Passa new SQLiteConnection(CapacitorSQLite).
Un errore "connessione già esistente" alla ricarica. Initialization ran twice. Keep the single ready promessa e chiamata checkConnectionsConsistency() prima initialize().
No metadata for "Post" was found. La classe entità non è nella entities array, o hai utilizzato una stringa glob. Importa la classe.
Riavvio dei dati dopo ogni aggiornamento dell'app durante lo sviluppo. synchronize: true Le migrazioni sono state eseguite sul __CAPGO_KEEP_0__ ma non sul dispositivo.
Migrazioni eseguite sul CLI ma non sul dispositivo. La nuova classe non è nella migrations array in data-source.tsL'app non esegue lo scansionamento delle cartelle.
Wrap-up
TypeORM su Capacitor è composto da tre parti: il plugin SQLite della community come driver, entità con tipi di colonna espliciti e migrazioni generate contro un database locale sqljs banche dati e importate come classi. Con queste in posizione, repository, relazioni e transazioni si comportano allo stesso modo del backend.
For una configurazione più leggera su @capgo/capacitor-fast-sqlvedi Kysely con Capacitor. Per le basi del plugin, inizia con Come utilizzare SQLite in un'app Capacitor. Quando il lato nativo cambia, Capgo Build Ri-costruisci e firma le app iOS e Android nel cloud.