Saltare al contenuto principale

Come utilizzare TypeORM con Capacitor e SQLite

Utilizza TypeORM con Capacitor e SQLite su iOS e Android: configurazione di DataSource, entità che funzionano con Vite, repository, transazioni e migrazioni generate.

Crediti dell'articolo

Martin Donadieu

Scrittore

Valeria

Recensore

Jordan

Curatore

Come utilizzare TypeORM con Capacitor e SQLite

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 tra author.ts e post.ts se mai compilare con metadati di decoratore (ad esempio nei CLI o nei test).
  • L'esplicito authorId colonna ti consente di filtrare e inserire per id senza caricare l'autore.
  • SQLite non ha un tipo booleano. TypeORM memorizza boolean le colonne come 0/1 e 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 utilizza SQLiteConnection. Il driver di TypeORM lo utilizza per creare e aprire la connessione, quindi attiva PRAGMA foreign_keys così onDelete: 'CASCADE' funziona.
  • database: il plugin aggiunge un SQLite.db suffix, 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 dopo initialize(), 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.

Aggiornamenti in tempo reale per Capacitor app

Quando un bug nel layer web è attivo, invia la correzione attraverso Capgo invece di attendere giorni per l'approvazione della store. Gli utenti ricevono l'aggiornamento in background mentre le modifiche native rimangono nel normale percorso di revisione.

Ultimi articoli dal nostro Blog

Capgo ti offre le migliori informazioni che ti servono per creare un'app mobile davvero professionale.