Zum Hauptinhalt springen

TypeORM mit Capacitor und SQLite verwenden

Verwenden Sie TypeORM mit Capacitor und SQLite auf iOS und Android: Datenquellenkonfiguration, Entitys, die mit Vite funktionieren, Repositories, Transaktionen und generierte Migrationen.

Artikelcredits

Martin Donadieu

Autor

Valeria

Rezensent

Jordan

Redakteur

Verwendung von TypeORM mit Capacitor und SQLite

TypeORM funktioniert in einer Capacitor-Anwendung über ihren integrierten capacitor Treiber, der jede Abfrage ausführt @capacitor-community/sqliteSie konfigurieren eine DataSource mit dem Plugin’s SQLiteConnection, define entities with decorators, and use repositories and transactions as you would on a server. This guide covers the full setup on TypeORM 1.x and Capacitor 8, including the two things most tutorials skip: explicit column types for Vite, and how to generate migrations even though the CLI cannot talk to a phone.

Diese Anleitung umfasst die vollständige Einrichtung in TypeORM 1.x und __CAPGO_KEEP_0__ 8, einschließlich der beiden Dinge, die die meisten Tutorials überspringen: explizite Spaltentypen für Vite und die Generierung von Migrationen, obwohl der __CAPGO_KEEP_1__ nicht mit einem Telefon kommunizieren kann.

TypeORM ist ein Datenmappungs-ORM mit Entitäten, Beziehungen, Repositorien und einem Migrationsystem. Auf mobilen Geräten macht es Sinn wenn:

  • Sie verwenden bereits TypeORM auf der Backend-Seite und möchten das gleiche Entitätsformat in der App haben.
  • Ihr Datenmodell hat viele Beziehungen und Sie möchten relations: { posts: true } anstatt manueller Join-Anweisungen
  • Ihr Team bevorzugt Klassen und Dekoratoren gegenüber Schemaobjekten.

Es ist schwerer als ein Query-Builder. Wenn Größe des Bundle und Startzeit am wichtigsten sind, oder Sie möchten @capgo/capacitor-fast-sql, look at sehen Sie sich or Drizzle anstatt.

Installieren

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

TypeORM 1.x-Importe reflect-metadata sich selbst, also benötigen Sie keine separate Import am Anfang main.ts as older guides show. The community plugin bundles SQLCipher on iOS and Android whether or not you encrypt, which matters when you answer App Store export compliance questions.

TypScript-Einstellungen

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

experimentalDecorators ermöglicht TypeORMs Legacy-Decorators. useDefineForClassFields: false TypeORM legt Werte fest, die von deiner Klasse überschrieben werden, was wichtig ist, wenn target ist ES2022 oder später.

Entitäten, die mit Vite und esbuild funktionieren

Die meisten Capacitor-Anwendungen werden mit Vite erstellt, das TypeScript mit esbuild kompiliert. esbuild unterstützt Dekoratoren, implementiert sie jedoch nicht. emitDecoratorMetadataso TypeORM kann die Eigenschaften nicht zur Laufzeit lesen. Ohne explizite Typen erhältst du ColumnTypeUndefinedError. Gib jedem Spalten einen Typ:

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

Anmerkungen:

  • Relation<T> ist eine Typen-Wrapper. Er vermeidet Kreislaufimportprobleme zwischen author.ts und post.ts wenn du jemals mit Dekoratorenmetadaten kompilierst (zum Beispiel in der CLI oder Tests).
  • Die explizite authorId Spalte lässt dich filtern und durch die ID einsetzen, ohne den Autor zu laden.
  • SQLite kennt keine boolesche Typen. TypeORM speichert boolean Spalten als 0/1 und konvertiert sie zurück.

Konfiguriere die Datenquelle

// 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 für Option:

  • driver: die Plugin-Einstellung SQLiteConnection. TypeORMs Treiber verwendet es, um die Verbindung zu erstellen und zu öffnen, und schaltet dann PRAGMA foreign_keys so onDelete: 'CASCADE' funktioniert.
  • database: das Plugin fügt ein SQLite.db Suffix, so dass das Datei appSQLite.db.
  • mode: no-encryptionoder eines der Plugins-Verschlüsselungsmodi (secret, encryptionSiehe Verschlüsseln von SQLite in Capacitor.
  • synchronize: falseProduktionsumgebungen immer. Sync kann Spalten löschen, um die Entitäten zu passen, was die Benutzerdaten auf dem Gerät löscht.
  • migrationsRun: false: die Plugin-Dokumentation fordert dies mit dem capacitor-Typ an. Führen Sie die Migrationen selbst direkt nach initialize(), wie unten gezeigt.
  • journalMode: 'WAL': optional. Bessere Konkurrenz zwischen Lesen und Schreiben.
  • migrations: Klassenreferenzen, nicht ein globales Muster. Es gibt keine Dateisystem-Überprüfung in einem WebView.

Glob-Muster wie entities: ['src/**/*.entity.ts'] funktionieren nicht im App für denselben Grund. Importieren Sie die Klassen.

Initialisieren Sie bei Start

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

Aufrufen await initDatabase() vor dem App-Rendern, bevor Daten gelesen werden. transaction: 'each' Führt jede Migration in einer eigenen Transaktion aus, sodass ein Fehler frühere Migrationsanwendungen belässt und die fehlgeschlagene zurückrollt.

Repositories: Einfügen, Abfragen, aktualisieren, löschen

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() ist schneller, weil es die zusätzlichen Abfragen vermeidet.

Query builder für alles, was nicht trivial ist

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 und Aggregaten sind das, wo ein ORM euch auf dem Mobilgerät Zeit spart. Überprüft die generierte SQL mit .getSql() während ihr entwickelt, und fügt Indexe für Spalten hinzu, die in WHERE und JOIN.

Transaktionen

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

Verwenden Sie manager übergeben, die an jede Abfrage innerhalb der Transaktion übergeben werden. Wenn der Callback auslöst, rollt TypeORM zurück.

Der Capacitor-Treiber verwendet eine einzelne Verbindung, daher gibt es keine Isolation zwischen der Transaktion und anderen Arbeiten: Eine Abfrage, die durch ds oder durch einen anderen Repository während der offenen Transaktion ausgeführt wird, läuft auf derselben Verbindung und wird entweder abgeschlossen oder zurückgerollt. Führt nicht unabhängige Datenbankarbeiten aus, bis die Transaktion abgeschlossen ist, halte Transaktionen kurz und wartet nicht auf eine Netzwerk-Anfrage innerhalb einer.

Migrations: Generieren sie mit einem lokalen sqljs-Datensatz

TypeORM CLI läuft auf eurem Computer und nicht auf dem Telefon, daher kann es die Gerätedatenbank nicht inspizieren. Die Plugin-Docs sagen, dass Migrations für den capacitor-Typ von Hand geschrieben werden müssen. Ihr könnt trotzdem generierte Migrations erhalten: TypeORMs sqljs Treiber verwendet dieselbe SQLite-Dialekt, daher produziert er SQL, das vom Gerät unverändert ausgeführt wird.

1. Fügen Sie eine CLI-spezifische Datenquelle hinzu

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

Add .typeorm/ to .gitignoreDas Datei ist eine Arbeitskopie von „wie das Schema aussieht, nachdem alle bestehenden Migrationen durchgeführt wurden“.

2. Aktualisieren Sie die Scratch-Datenbank und diffenziieren Sie sie dann

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 runs the TypeScript data source and entities directly. The first command applies existing migrations to the scratch database. The second compares it with your entities and writes a new migration class, for example src/db/migrations/1790000123456-AddTags.tsDann fügen Sie diese Klasse hinzu. migrations array in data-source.ts.

Eine generierte Anfangsmigration sieht so aus:

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

Bevor Sie eine Migration verschicken, lesen Sie sie sorgfältig durch. Für SQLite ändert TypeORM oft eine Tabelle, indem sie eine neue erstellt. temporary_<table>, copying rows, dropping the original and renaming. That is correct, but on a large table it takes time on a phone, so run it with a loading state.

Migrations manuell erstellen

If you write migrations yourself, start every statement with an uppercase keyword. The capacitor query runner looks at the first word, case-sensitively, to decide how to call the plugin: BEGIN, COMMIT, ROLLBACK, CREATE, ALTER und DROP gehen Sie zu execute, INSERT, UPDATE und DELETE gehen Sie zu run, und alles andere geht zu query. Ein kleines create table wird als Abfrage gesendet, was nicht das ist, was Sie für DDL wollen.

Migrations und Live-Updates

Migrationen sind Teil Ihres JavaScript-Bundles. Mit Capgo Live-Updates wird eine Schemaänderung den Benutzern ohne eine Store-Bewertung erreicht. Ein Rollover kann dann ältere code auf ein neueres Schema setzen, daher bevorzugen Sie additive Migrations (neue Tabellen, neue nullable-Spalten) und entfernen Sie alte Spalten in einer späteren Version.

Web-Unterstützung

Die Community-Plugin läuft auf der Web-Plattform durch das jeep-sqlite Web-Komponente (sql.js persistiert in IndexedDB). Es ist nützlich für die Entwicklung im 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');
}

Auf der Web-Plattform lebt die Datenbank im Speicher und wird in IndexedDB geschrieben, wenn Sie sqliteConnection.saveToStore('app'). Call it after writes you care about, or data is lost on reload.

Fehlerbehebung

Column type for Post#title is not defined and cannot be guessed. esbuild emittiert keine Metadaten. Fügen Sie eine explizite Typdefinition hinzu zum @Column decorator.

Capacitor package has not been found installed. The driver Option fehlt oder ist undefiniert. Pass new SQLiteConnection(CapacitorSQLite).

Option ist fehlend oder undefiniert. Geben Sie Ein Fehler "Verbindung bereits existent" bei einem Neuladen. ready Versprechen und Aufrufen checkConnectionsConsistency() vorher initialize().

No metadata for "Post" was found. Die Entitätsklasse ist nicht im entities array oder Sie haben ein glob-String verwendet. Importieren Sie die Klasse.

Die Daten werden nach jedem App-Update im Entwicklungsmodus zurückgesetzt. synchronize: true is on, or migrations drop and recreate tables. Turn sync off and inspect the generated SQL.

Die Migrations wurden auf CLI durchgeführt, aber nicht auf dem Gerät. Die neue Klasse befindet sich nicht im Array in migrations array in data-source.tsDie App scannt keine Ordner.

Wrap-up

TypeORM auf Capacitor besteht aus drei Teilen: der Community-Plugin für SQLite als Treiber, Entitäten mit expliziten Spalten-Typen und Migrations, die gegen einen lokalen Datenbank-Server generiert werden. sqljs Datenbank und importiert als Klassen. Mit diesen im Auge behalten, verhalten sich Repositories, Beziehungen und Transaktionen gleich wie auf Ihrem Backend.

Für eine leichtere Konfiguration auf @capgo/capacitor-fast-sql, siehe Kysely mit CapacitorFür die Grundlagen des Plugins, beginnen Sie mit Wie Sie SQLite in einer Capacitor-Anwendung verwenden. Wenn die native Seite sich ändert, Capgo Build kann die iOS- und Android-App im Cloud neu erstellen und signieren.

Live Updates für Capacitor-Apps

Wenn ein Web-Schicht-Bug live ist, schicken Sie die Reparatur über Capgo anstatt Tage für die Genehmigung im App-Store zu warten. Die Benutzer erhalten die Aktualisierung im Hintergrund, während native Änderungen im normalen Review-Prozess bleiben.

Neueste von unserem Blog

Capgo gibt Ihnen die besten Einblicke, die Sie benötigen, um ein wirklich professionelles Mobiltelefon-App zu erstellen.