メインコンテンツにスキップ

TypeORMを使用してCapacitorとSQLiteで

TypeORMを使用してCapacitorとSQLiteでiOSとAndroidを実行する: DataSourceの設定、Viteで動作するエンティティ、リポジトリ、トランザクション、生成されたマイグレーション。

Article credits

マーティン・ドナディュー

ライター

ヴァレリア

レビュアー

ジョーダン

エディター

How to Use TypeORM with Capacitor and SQLite

TypeORM works in a Capacitor app through its built-in capacitor ドライバーを使用して、 @capacitor-community/sqliteを使用して構成します。 DataSource プラグインの SQLiteConnection、エンティティをデコレータで定義し、リポジトリとトランザクションをサーバー上で使用するようにします。このガイドでは、TypeORM 1.xとCapacitor 8のフルセットアップをカバーし、2つのチュートリアルが省略していること、Vite用の明示的な列タイプ、そしてCLIが電話に接続できないため、生成されるマイグレーションを含みます。

TypeORMが適している場合

TypeORMはエンティティ、リレーション、リポジトリ、そしてマイグレーションシステムを備えたデータマッパーORMです。モバイル上では、次の場合に意味があります:

  • 既存のバックエンドでTypeORMを使用している場合、同じエンティティスタイルをアプリケーションに適用したい場合
  • データモデルが多くのリレーションを持ち、手動でJOINを書きたくない場合 relations: { posts: true } チームがクラスとデコレータを、スキーマオブジェクトよりも好みます。
  • Query Builderよりも重いです。bundleサイズと起動時間が最も重要な場合、またはQuery Builderを使用したい場合、Kyselyまたはを参照してください。

Kysely @capgo/capacitor-fast-sqlまたは Kysely or Drizzle 代わりに。

インストール

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

TypeORM 1.xのインポート reflect-metadata Capacitor では、TypeORM を SQLite に使用する場合、TypeORM をインポートする必要はありません。 main.ts TypeScriptの設定

TypeORMの古いデコレータを有効にします。

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

experimentalDecorators TypeORMの古いデコレータを有効化します。 useDefineForClassFields: false Viteとesbuildで動作するエンティティ target ほとんどの__CAPGO_KEEP_0__アプリケーションはViteで構築されており、esbuildでTypeScriptをコンパイルします。esbuildはデコレータをサポートしていますが、実装していません。

Install

Most Capacitor apps are built with Vite, which compiles TypeScript with esbuild. esbuild supports decorators but does not implement emitDecoratorMetadata、TypeORMはプロパティのタイプを実行時で読むことができないため、明示的なタイプがなければ ColumnTypeUndefinedErrorをすべての列にタイプを与える

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

メモ:

  • Relation<T> はタイプのみのラッパーです。 これは、TypeORMとTypeORMの間の循環依存問題を回避するために使用されます。 author.ts と post.ts もし、デコレーターメタデータでコンパイルする場合(例えば、CLI またはテストで)。
  • もしもあなたがデコレーターメタデータでコンパイルする場合 (例えば、__CAPGO_KEEP_0__ またはテストで)。 authorId 明示的な
  • 列は、作者をロードすることなくIDでフィルタリングおよび挿入できるようにします。 boolean columns as 0/1 を列として保存し、

に変換します。

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

オプションごとに:

  • driver: プラグインの SQLiteConnection. TypeORMのドライバーは、接続を作成し、オープンし、接続を有効にし、 PRAGMA foreign_keys で onDelete: 'CASCADE' が機能します。
  • database: プラグインは SQLite.db を追加するため、ファイルは appSQLite.db.
  • mode: no-encryption、またはプラグインの暗号化モードのsecret, encryption). See encrypting SQLite in Capacitor.
  • synchronize: false: 生産環境では常に。Syncはエンティティを合わせるために列を削除することができますが、これはデバイス上のユーザーデータを削除します。
  • migrationsRun: false: プラグインのドキュメントはこのタイプのcapacitorを要求しています。マイグレーションを実行するには、直後に initialize()、以下のようになります。
  • journalMode: 'WAL': 必須。読み取りと書き込みの並行性が向上します。
  • migrations: クラス参照、ファイルシステムスキャンはありません。

パターンは entities: ['src/**/*.entity.ts'] 、アプリ内では機能しません。クラスをインポートしてください。

アプリ起動時に初期化

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

を呼び出してください。 await initDatabase() データを読むものが表示される前に transaction: 'each' を呼び出してください。

レポジトリ: インサート、セレクト、アップデート、デリート

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() レポジトリ: インサート、セレクト、更新、削除 insert() 検索が省略されるため、処理が速くなります。

非凡なもののクエリビルダー

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

JOIN と AGGREGATE は、モバイルで ORM が時間を節約する場所です。生成された SQL を .getSql() 開発中、カラムが使用されている WHERE と JOIN.

トランザクション

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

使用する manager コールバックに渡されるすべてのクエリに対して実行されます。コールバックが例外を投げると、TypeORM はロールバックします。

Capacitorドライバーは、1つの接続を使用するため、トランザクションと他の作業間の隔離はありません: Capacitorを通じて実行されたクエリは ds マイグレーション: ローカル sqljs データソースを使用して生成してください

Migrations: __CAPGO_KEEP_0__をローカルsqljsデータソースで生成する

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 ドライバーは同じ SQLite 方言を使用するため、デバイスで実行することなく SQL を生成します。

1. CLIの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'],
});

追加 .typeorm/ to .gitignoreそのファイルは、すべての既存のマイグレーションを適用した後、スキーマがどのように見えるかを表すスクラッチコピーです。

2. スクラッチデータベースを最新の状態にし、差分を取得します。

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はTypeScriptデータソースとエンティティを直接実行します。最初のコマンドは、スクラッチデータベースに既存のマイグレーションを適用します。2番目のコマンドは、エンティティと比較し、新しいマイグレーションクラスを書き出します。たとえば src/db/migrations/1790000123456-AddTags.ts。次に、そのクラスを migrations 配列に追加します。 data-source.ts.

初期マイグレーションは次のようになります。

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

SQLiteでTypeORMがテーブルを変更する場合、TypeORMはテーブルを削除してから新しいテーブルを作成します。 temporary_<table>手作業でマイグレーションを書く

手動でマイグレーションを書く

SQL マイグレーションを自分で書く場合、すべてのステートメントは大文字のキーワードで始まる必要があります。 capacitor クエリ ランナーは、最初の単語をケース センシティブに検討して、プラグインを呼び出す方法を決定します: BEGIN, COMMIT, ROLLBACK, CREATE, ALTER そして DROP 行く execute, INSERT, UPDATE そして DELETE 行く runDB にアクセス queryマイグレーション クラスは JavaScript バンドルの一部です。 __CAPGO_KEEP_0__ ライブ アップデート create table DDLの場合、DDLコマンドはクエリとして送信されます。

マイグレーションとライブアップデート

マイグレーションクラスはJavaScriptバンドルの一部です。With Capgo ライブアップデート 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.

Webサポート

コミュニティプラグインは、Web上で実行されます。 jeep-sqlite Webコンポーネント (sql.jsはIndexedDBに永続化されます)。ブラウザ上での開発に役立ちます。

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

Web上のデータベースはメモリ内にあり、書き込み時にIndexedDBに書き込まれます。 sqliteConnection.saveToStore('app')書き込み後に重要なデータを保存したい場合は、.を呼び出してください。再読み込み時にデータが失われるのを防ぎます。

トラブルシューティング

Column type for Post#title is not defined and cannot be guessed. esbuildはメタデータを出力しませんでした。明示的な型を追加してください。 @Column decorator.

Capacitor package has not been found installed. The driver パラメータを渡してください。 new SQLiteConnection(CapacitorSQLite).

再読み込み時に「接続が既に存在する」エラーが発生します。 初期化が2回実行されました。シングルを維持してください。 ready 約束と呼び出し checkConnectionsConsistency() 前 initialize().

No metadata for "Post" was found. 機能に取り組む前に問題を提起して議論する entities 機能に取り組む前に問題を提起して議論する

エンティティクラスは配列に含まれていません、またはグローバル文字列を使用しました。クラスをインポートしてください。 synchronize: true 開発中のアプリの更新ごとにデータがリセットされます。

Migrations ran in the CLI but not on the device. マイグレーションは__CAPGO_KEEP_0__で実行されましたが、デバイスでは実行されませんでした。 migrations array in data-source.ts配列

.

TypeORM on Capacitor は、コミュニティSQLiteプラグインをドライバーとして、エンティティの明示的なカラムタイプ、そしてローカルに生成されたマイグレーションという3つの部分で構成されます。 sqljs データベースとクラスとしてインポートされます。 それらが置かれたら、リポジトリ、関係、トランザクションはバックエンドで同じように動作します。

より軽量なセットアップについては @capgo/capacitor-fast-sqlを参照してください Kysely with Capacitorの基本的なプラグインについては SQLiteをCapacitorアプリで使う方法のアプリケーションでSQLiteを使用する方法 Capgo ビルド ネイティブ側が変更されたとき

Capacitorのリアルタイムアップデート

Capgoのリアルタイムアップデートの場合、ウェブ層のバグが生じた場合、修正をCapgoで配信し、数日間待つ必要のないアプリストアの承認を待つのではなく、ユーザーはバックグラウンドで更新を受け取り、ネイティブの変更は通常のレビュー経路で残ります。

最新のブログ

Capgoは、プロフェッショナルなモバイルアプリを作成するために必要な最良の洞察を提供します。