跳过主要内容

如何使用 TypeORM 与 Capacitor 和 SQLite

使用 TypeORM 与 Capacitor 和 SQLite 在 iOS 和 Android 上:数据源设置、与 Vite 兼容的实体、仓库、事务和生成的迁移。

文章来源

马丁·多纳迪厄

作者

瓦莱里娅

审稿人

乔丹

编辑

如何使用 TypeORM 与 Capacitor 和 SQLite

TypeORM 在 Capacitor 应用程序中通过其内置的 capacitor 驱动器,所有查询都在 @capacitor-community/sqlite上运行。您配置一个 DataSource 与插件一起 SQLiteConnection使用装饰器定义实体,并像在服务器上一样使用仓库和事务。这篇指南涵盖了TypeORM 1.x和Capacitor 8的完整设置,包括两个大多数教程都忽略的内容:Vite的明确列类型,以及如何在CLI无法与手机通信时生成迁移。

什么时候TypeORM是一个合适的选择

TypeORM是一个数据映射器ORM,具有实体、关系、仓库和迁移系统。在移动端,它有意义时:

  • 你已经在后端使用TypeORM,并希望在应用中使用相同的实体风格
  • 你的数据模型有很多关系,你希望 relations: { posts: true } 而不是手写连接
  • 你的团队更喜欢使用类和装饰器而不是schema对象。

它比一个查询构建器更重。如果包大小和启动时间最重要,或者你想使用 @capgo/capacitor-fast-sql看看 Kysely 或 Drizzle 取代。

安装

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

TypeORM 1.x 导入 reflect-metadata 不需要单独导入它本身,因此您不需要在顶部添加单独的导入 main.ts TypeScript 设置

启用 TypeORM 的遗留装饰器。

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

experimentalDecorators 停止类字段初始化器覆盖 TypeORM 设置的值,这在您的 useDefineForClassFields: false 是 ES2022 或更高版本时很重要。 target 与 Vite 和 esbuild 一起工作的实体

与 Vite 和 esbuild 一起工作的实体

大多数Capacitor应用程序是使用Vite构建的,Vite会将TypeScript编译为esbuild。 esbuild支持装饰器,但不实现它们。 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> 是一个类型仅包装器。它避免了导入循环问题之间的 author.ts 和 post.ts 如果您曾经使用装饰器元数据编译(例如在CLI或测试中)。
  • 如果你曾经编译过装饰器元数据(例如在 __CAPGO_KEEP_0__ 或测试中)。 authorId 的明确
  • 列让你可以通过 id 过滤和插入而不需要加载作者。 boolean columns 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)。参见 在 Capacitor 中的 SQLite 加密:.
  • synchronize: false在生产环境中,总是。 同步可以删除列以匹配实体,这会删除设备上的用户数据。
  • migrationsRun: false: 插件的文档要求使用 capacitor 类型。 在运行迁移之后 initialize(),如下所示。
  • journalMode: 'WAL':可选。改善读写操作之间的并发性。
  • migrations:类引用,而不是通配符。WebView中没有文件系统扫描。

通配符模式类似于 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() context transaction: 'each' HTML文本片段来自更长的Capgo UI字符串(父键`appflow_migration_step2`)。页面/区域:Appflow比较/迁移营销复制。角色:网站复制句子。见于:页面替代项/ionic-appflow.astro。保留Capgo产品/品牌和开发人员术语的原始形式。消息键`appflow_migration_step2`(Appflow迁移步骤2)。

在您的应用程序渲染任何读取数据的内容之前

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

连接和聚合是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驱动时,会共用一个连接,因此事务和其他工作之间没有隔离:通过该驱动执行的查询 ds 驱动程序使用单个连接,因此事务和其他工作之间没有隔离:在事务期间通过

或另一个仓库中的查询运行在同一连接上,并且会与它一起提交或回滚。不要在事务完成之前开始与数据库无关的工作,保持事务短暂,

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 TypeORM

1. 添加一个仅 CLI 的数据源

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/ 到 .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 数据源和实体。第一个命令将现有迁移应用到草稿数据库。第二个命令将其与您的实体进行比较,并写入一个新的迁移类,例如 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 经常通过创建 temporary_<table>,复制行,删除原始表并重命名来更改表。这是正确的,但在手机上处理大表时会花费时间,所以请在加载状态下运行它。

手动编写迁移

如果您自己编写迁移,所有语句的第一行必须以大写关键字开头。capacitor 查询运行器根据第一个单词(大小写敏感)来决定如何调用插件: BEGIN, COMMIT, ROLLBACK, CREATE, ALTER and DROP 前往 execute, INSERT, UPDATE and DELETE 前往 run, 其余内容都将 query。一个小写 create table 会发送一个查询,这不是您想要的DDL。

迁移和实时更新

迁移类是您的JavaScript包的一部分。使用 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 上,数据库存储在内存中,当您调用 sqliteConnection.saveToStore('app')时会将其写入 IndexedDB:。在写入后调用它关心的写入,否则在重新加载时会丢失数据。

故障排除

Column type for Post#title is not defined and cannot be guessed. esbuild 未输出元数据。请为其添加明确的类型。 @Column decorator.

Capacitor package has not been found installed. 装饰器。 driver 选项缺失或未定义。传递 new SQLiteConnection(CapacitorSQLite).

重新加载时出现“连接已存在”错误。 初始化已运行两次。保持单个 ready Promise 和调用 checkConnectionsConsistency() 在工作于新功能之前 initialize().

No metadata for "Post" was found. 在工作于新功能之前,首先创建一个问题并进行讨论。 entities 实体类不在数组中,或您使用了一个 glob 字符串。导入类。

在开发过程中,每次应用更新后都会重置数据。 synchronize: true 如果开启了同步,或者迁移会删除并重新创建表格。关闭同步并检查生成的 SQL。

迁移在 CLI 上运行,但在设备上没有运行。 实体类不在数组中 migrations 在 data-source.ts。应用程序不会扫描文件夹。

总结

TypeORM 在 Capacitor 上是三部分:社区 SQLite 插件作为驱动,实体类具有明确的列类型,和针对本地数据库生成的迁移 sqljs 数据库和导入为类。有了这些,仓库、关系和事务与您的后端一样工作。

为轻量级设置在 @capgo/capacitor-fast-sql,见 Kysely与Capacitor。有关插件的基本知识,请从 如何在Capacitor应用中使用SQLite。当原生侧发生变化时, Capgo 构建 。

实时更新的Capacitor应用

当web层bug活跃时,通过Capgo将修复推送给用户,而不是等待几天的应用商店审批。用户在后台接收更新,而原生变化仍在正常审批路径中。

最新博客

Capgo为您提供创建真正专业的移动应用所需的最佳见解。