Passer à la navigation principale

Comment utiliser TypeORM avec Capacitor et SQLite

Utilisez TypeORM avec Capacitor et SQLite sur iOS et Android : configuration de la source de données, entités qui fonctionnent avec Vite, répositories, transactions et migrations générées.

Crédits de l'article

Martin Donadieu

Auteur

Valeria

Réviseur

Jordan

Éditeur

Comment utiliser TypeORM avec Capacitor et SQLite

TypeORM fonctionne dans une application Capacitor grâce à son module intégré. capacitor driver, qui exécute chaque requête sur @capacitor-community/sqliteVous configurez un DataSource avec le plugin’s SQLiteConnection, définissez des entités avec des décorateurs, et utilisez des répositories et des transactions comme vous le feriez sur un serveur. Cette guide couvre la mise en place complète sur TypeORM 1.x et Capacitor 8, y compris les deux choses que les tutoriels ignorent généralement : les types de colonnes explicites pour Vite, et comment générer des migrations même si le CLI ne peut pas communiquer avec un téléphone.

Quand TypeORM est un bon choix

TypeORM est un ORM de données-mappage avec des entités, des relations, des répositories et un système de migration. Sur mobile, cela a du sens quand :

  • vous utilisez déjà TypeORM sur le backend et que vous voulez le même style d'entité dans l'application
  • votre modèle de données a de nombreuses relations et que vous voulez relations: { posts: true } au lieu de joints manuels
  • votre équipe préfère les classes et les décorateurs plutôt que les objets de schéma.

C'est plus lourd qu'un constructeur de requête. Si la taille du bundle et le temps d'initialisation comptent le plus, ou si vous souhaitez utiliser @capgo/capacitor-fast-sqlRegardez plutôt Kysely ou Drizzle au lieu de cela.

Installer

bun add typeorm @capacitor-community/sqlite
bunx cap sync

les importations TypeORM 1.x reflect-metadata se font elles-mêmes, vous n'avez donc pas besoin d'une importation séparée en haut de main.ts les anciens guides montrent. Le plugin de la communauté embarque SQLCipher sur iOS et Android, que vous encryptiez ou non, ce qui compte lorsqu'il vous pose des questions de conformité à l'exportation de l'App Store.

Paramètres de TypeScript

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "useDefineForClassFields": false
  }
}

experimentalDecorators active les décorateurs de TypeORM. useDefineForClassFields: false arrête les initialisateurs de champs de classe de surécrire les valeurs que TypeORM définit, ce qui compte lorsque votre target est ES2022 ou ultérieur.

Les entités qui fonctionnent avec Vite et esbuild

La plupart des applications Capacitor sont construites avec Vite, qui compile TypeScript avec esbuild. esbuild prend en charge les décorateurs mais ne les implémente pas. emitDecoratorMetadata, donc TypeORM ne peut pas lire les types en temps de exécution. Sans types explicites, vous obtenez ColumnTypeUndefinedError : donnez un type à chaque colonne

// 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>;
}

Notes :

  • Relation<T> est un wrapper de type uniquement. Il évite les problèmes d'import circulaire entre author.ts et post.ts Si vous compilez jamais avec des métadonnées de décorateur (par exemple, dans le CLI ou les tests).
  • L’explicite authorId la colonne vous permet de filtrer et d'insérer par l'ID sans charger l'auteur.
  • SQLite n'a pas de type booléen. TypeORM stocke boolean columns 0/1 et les convertit à nouveau.

Configurez la source de données

// 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'],
});

Option par option :

  • driver : le plugin utilise SQLiteConnection . Le pilote TypeORM utilise cela pour créer et ouvrir la connexion, puis active PRAGMA foreign_keys donc onDelete: 'CASCADE' fonctionne.
  • database : le plugin ajoute un SQLite.db suffixe, donc le fichier est appSQLite.db.
  • mode: no-encryption ou l'un des modes de cryptage du plugin (secret, encryption ). Voir : chiffrer SQLite dans Capacitor .
  • synchronize: false: in production, always. Sync can drop columns to match entities, which deletes user data on the device.
  • migrationsRun: false : la documentation du plugin demande ceci avec le type capacitor . Exécutez les migrations vous-même juste après initialize(), comme indiqué ci-dessous.
  • journalMode: 'WAL': facultatif. Meilleure concurrence entre lectures et écritures.
  • migrationsPas de référencement de classe, pas de balayage du système de fichiers dans un WebView.

Glob des modèles comme entities: ['src/**/*.entity.ts'] ne fonctionnent pas dans l'application pour la même raison. Importez les classes.

Initialisez à l'arrêt.

// 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;
}

Appelez await initDatabase() avant que votre application affiche quoi que ce soit et lit des données. transaction: 'each' wrappe chaque migration dans sa propre transaction, de sorte qu'une erreur laisse les migrations précédentes appliquées et la migration échouée annulée.

Les repositories : insérer, sélectionner, mettre à jour, supprimer

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() est plus rapide car il évite les recherches supplémentaires.

Constructeur de requêtes pour les cas non triviaux.

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 }>();

Joins et agrégats sont là où un ORM vous économise du temps sur mobile. Vérifiez le SQL généré avec .getSql() en développant, et ajoutez des index pour les colonnes utilisées dans WHERE et JOIN.

Transactions

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 });
});

Utilisez le manager transmis au callback pour chaque requête à l'intérieur de la transaction. Si le callback lance une erreur, TypeORM annule la transaction.

Le pilote Capacitor utilise une seule connexion, il n'y a donc pas d'isolation entre la transaction et d'autres travaux : une requête effectuée à travers ds ou dans un autre repository pendant que la transaction est ouverte s'exécute sur la même connexion et est commitée ou annulée avec elle. N'entamez pas de travaux de base de données non liés avant la fin de la transaction, gardez les transactions courtes et n'attendez jamais une requête réseau à l'intérieur d'une.

Migrations : générer-les avec une DataSource sqljs locale

The TypeORM CLI runs on your computer, not on the phone, so it cannot inspect the device database. The plugin’s own docs say migrations for the capacitor type have to be written by hand. You can still get generated migrations: TypeORM’s sqljs le pilote utilise le même dialecte SQLite, il produit donc du SQL qui fonctionne sans modification sur le dispositif.

1. Ajoutez une CLI-seule source de données

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'],
});

Ajouter .typeorm/ à .gitignore. Ce fichier est une copie de « ce que le schéma ressemble après toutes les migrations existantes ».

2. Mettez à jour la base de données de scratch, puis comparez

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 exécute la source de données TypeScript et les entités directement. La première commande applique les migrations existantes à la base de données de scratch. La deuxième compare celle-ci avec vos entités et écrit une nouvelle classe de migration, par exemple src/db/migrations/1790000123456-AddTags.ts. Ensuite, ajoutez ensuite cette classe à la migrations tableau dans data-source.ts.

Une migration initiale générée ressemble à ceci :

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"`);
  }
}

Lisez chaque migration générée avant de la livrer. Pour SQLite, TypeORM modifie souvent une table en créant temporary_<table>, en copiant des lignes, en supprimant l'original et en renommant. C'est correct, mais sur une grande table, cela prend du temps sur un téléphone, il faut donc le lancer avec un état de chargement.

Écrire des migrations à la main

Si vous écrivez vous-même les migrations, commencez chaque instruction par un mot-clé en majuscule. Le capacitor exécuteur de requêtes examine le premier mot, de manière sensible à la casse, pour décider comment appeler le plugin : BEGIN, COMMIT, ROLLBACK, CREATE, ALTER et DROP aller vers execute, INSERT, UPDATE et DELETE aller vers runet tout le reste va à queryUn mot en minuscule create table est envoyé comme une requête, ce qui n'est pas ce que vous voulez pour DDL.

Migrations et mises à jour en temps réel

Les classes de migration font partie de votre bundle JavaScript. Avec Capgo mises à jour en direct a schema change reaches users without a store review. A rollback can then put older code on a newer schema, so prefer additive migrations (new tables, new nullable columns) and remove old columns in a later release.

Support web

La plugin de la communauté fonctionne sur le web à travers le jeep-sqlite Le composant web (sql.js persisté dans IndexedDB). Il est utile pour le développement dans le navigateur :

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');
}

Sur le web, la base de données vit en mémoire et est écrite dans IndexedDB lorsque vous appelez sqliteConnection.saveToStore('app')Appelez-l’après les écritures dont vous vous souciez, ou les données sont perdues à la recharge.

Troubleshooting

Column type for Post#title is not defined and cannot be guessed. Le fichier esbuild n'a pas émis de métadonnées. Ajoutez un type explicite au @Column Le

Capacitor package has not been found installed. The driver Une erreur "connexion déjà existante" à la recharge. new SQLiteConnection(CapacitorSQLite).

L'initialisation a été exécutée deux fois. Gardez le seul L'initialisation a été lancée deux fois. Gardez le seul. ready et promesse d'appel checkConnectionsConsistency() avant initialize().

No metadata for "Post" was found. La classe d'entité n'est pas dans le entities Avant de travailler sur une nouvelle fonctionnalité, mentionnez le problème.

Data reset after every app update during development. synchronize: true is on, or migrations drop and recreate tables. Turn sync off and inspect the generated SQL.

Les migrations ont été exécutées dans le CLI mais pas sur le dispositif. La nouvelle classe n'est pas dans le migrations array en data-source.tsLa nouvelle classe n'est pas dans l'

array dans

TypeORM sur Capacitor est composé de trois éléments : le plugin SQLite de la communauté en tant que pilote, des entités avec des types de colonnes explicites et des migrations générées contre un serveur local. sqljs et importés en tant que classes. Avec ceux-ci en place, les repositories, les relations et les transactions se comportent de la même manière que sur votre backend.

For une mise en place plus légère sur @capgo/capacitor-fast-sql, voir Kysely avec Capacitor. Pour les bases du plugin, commencez avec Comment utiliser SQLite dans une Capacitor app. Lorsque le côté natif change, Capgo Build Pouvez reconstruire et signer les applications iOS et Android dans le cloud.

Mises à jour instantanées pour les applications Capacitor

Lorsqu'une bug de la couche web est en ligne, expédiez la correction par Capgo au lieu d'attendre des jours pour l'approbation de la boutique d'applications. Les utilisateurs obtiennent la mise à jour en arrière-plan tandis que les modifications natives restent dans la voie de revue normale.

Dernières actualités de notre Blog

Capgo vous donne les meilleures informations dont vous avez besoin pour créer une application mobile vraiment professionnelle.