TypeORM 通过其内置的 Capacitor 应用 capacitor driver,在以下环境运行每个查询: @capacitor-community/sqlite。你可以配置一个 DataSource 配合该插件的 SQLiteConnection,使用装饰器定义实体,并像在服务器端一样使用 repository 和 transaction。本指南涵盖了 TypeORM 1.x 和 Capacitor 8 的完整配置,包括大多数教程都会忽略的两点:为 Vite 配置显式的列类型,以及在 CLI 无法直接连接手机的情况下如何生成 migration。
何时适合使用 TypeORM
TypeORM 是一个包含实体 (entities)、关系 (relations)、repository 和 migration 系统的 data-mapper ORM。在移动端,如果满足以下情况,使用它会非常合适:
- 你已经在后端使用了 TypeORM,并希望在 App 中保持相同的实体风格,
- 你的数据模型包含大量关系,并且你希望
relations: { posts: true }而不是手动编写 join 查询, - 你的团队更倾向于使用 class 和 decorator,而不是 schema 对象。
它比 query builder 更重。如果 bundle size 和启动时间至关重要,或者你想使用 @capgo/capacitor-fast-sql,请查看 Kysely 或 Drizzle 作为替代。
安装
bun add typeorm @capacitor-community/sqlite
bunx cap sync
TypeORM 1.x 会自动导入 reflect-metadata 自身,因此你不需要像旧指南那样在顶部 main.ts 进行单独导入。无论你是否使用加密,该社区插件都会在 iOS 和 Android 上打包 SQLCipher,这在回答 App Store 出口合规性问题时非常重要。
TypeScript 设置
{
"compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false
}
}
experimentalDecorators 可以启用 TypeORM 的旧版装饰器。 useDefineForClassFields: false 防止类字段初始化程序覆盖 TypeORM 设置的值,这在你的 target 是 ES2022 或更高版本时非常重要。
兼容 Vite 和 esbuild 的 Entity
大多数 Capacitor 应用都使用 Vite 构建,它通过 esbuild 编译 TypeScript。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 或测试中进行编译时)。- 显式的
authorId列允许你在不加载作者的情况下,通过 id 进行过滤和插入。 - SQLite 没有布尔类型。TypeORM 会将
boolean列存储为0/1并在读取时将其转换回来。
配置 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'],
});
逐项配置:
driver:插件的SQLiteConnection. TypeORM 的驱动程序使用它来创建并打开连接,然后启用PRAGMA foreign_keys所以onDelete: 'CASCADE'可以正常工作。database: 插件会添加一个SQLite.db后缀,因此文件名为appSQLite.db.mode:no-encryption,或者是插件的加密模式之一 ("secret,encryption)。请参阅 在 Capacitor 中对 SQLite 进行加密.synchronize: false: 在生产环境中务必开启。Sync 可能会为了匹配 entity 而删除列,从而导致设备上的用户数据丢失。migrationsRun: false:插件文档要求使用 capacitor 类型。请在之后立即手动运行迁移initialize(),如下所示。journalMode: 'WAL':可选。可提高读写并发性能。migrations:引用类而非 glob 模式。WebView 中无法进行文件系统扫描。
像以下的 Glob 模式 entities: ['src/**/*.entity.ts'] 由于同样的原因在 App 中无法工作。请直接导入类。
在启动时初始化
// 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() 在 App 渲染任何读取数据的组件之前。 transaction: 'each' 它将每次迁移都封装在独立的事务中,因此如果发生失败,之前的迁移会保留,而失败的迁移会回滚。
Repository:insert, select, update, delete
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() 根据主键是否已设置来执行插入或更新,并返回带有生成 ID 的实体。对于不需要返回实体的批量写入操作, insert() 由于跳过了额外的查询,速度更快。
适用于处理复杂逻辑的 Query builder
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 和聚合操作是 ORM 节省时间的核心。可以通过以下方式检查生成的 SQL: .getSql() 在开发过程中,并为用于以下内容的列添加索引: WHERE 和 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 });
});
使用 manager 在事务内的每个查询中都会传递给回调函数。如果回调抛出异常,TypeORM 会执行回滚。
由于 Capacitor 驱动程序使用单一连接,因此事务与其他操作之间不存在隔离:在事务开启期间,通过 ds 或其他 repository 发起的查询将在同一连接上运行,并随事务一起提交或回滚。请勿在事务完成前启动无关的数据库操作,保持事务简短,且绝不要在事务内部等待网络请求。
Migrations:使用本地 sqljs DataSource 生成它们
TypeORM CLI 运行在你的电脑上而非手机上,因此无法检查设备数据库。插件文档指出,capacitor 类型的 migrations 必须手动编写。你仍然可以获取生成的 migrations:通过 TypeORM 的 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/ 至 .gitignore。该文件是“所有现有迁移完成后模式(schema)状态”的一个临时副本。
2. 更新临时数据库,然后进行 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 直接运行 TypeScript 数据源和实体。第一个命令将现有迁移应用于临时数据库。第二个命令将其与您的实体进行比较并写入新的 migration 类,例如 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"`);
}
}
生成的初始 migration 如下所示: temporary_<table>在发布之前请务必检查每一份生成的 migration。对于 SQLite,TypeORM 通常通过创建新表、复制行、删除原表并重命名的方式来修改表。这种操作是正确的,但在处理大表时在手机上会很耗时,因此请配合 loading 状态使用。
手动编写迁移文件
如果你手动编写迁移,请确保每个语句都以大写关键字开头。capacitor query runner 会通过区分大小写的首词来决定如何调用插件: BEGIN, COMMIT, ROLLBACK, CREATE, ALTER 和 DROP 转交给 execute, INSERT, UPDATE 和 DELETE 转交给 run,而其他所有内容都转交给 query。如果使用小写 create table ,它会被当作普通查询发送,这不符合 DDL 的预期。
迁移与 Live Update
迁移类是 JavaScript bundle 的一部分。通过 Capgo Live Update Schema 变更无需经过应用商店审核即可触达用户。通过回滚,可以将旧版本的 code 应用于新 Schema,因此建议优先采用增量迁移(如新增表、新增可为空的列),并在后续版本中再删除旧列。
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 装饰器添加显式类型。
Capacitor package has not been found installed. 该 driver 选项缺失或未定义。请传入 new SQLiteConnection(CapacitorSQLite).
页面刷新时出现 “connection already exists” 错误。 初始化运行了两次。请保留单个 ready promise 并调用 checkConnectionsConsistency() 在 initialize().
No metadata for "Post" was found. 实体类不在 entities 数组中,或者你使用了 glob 字符串。请导入该类。
开发期间,每次应用更新后数据都会重置。 synchronize: true 已开启,或者迁移操作会删除并重建表。请关闭 sync 并检查生成的 SQL。
Migrations 已在 CLI 中运行,但未在设备上运行。 新类不在 migrations 中的 data-source.ts数组里。应用不会扫描文件夹。
总结
在 Capacitor 上使用 TypeORM 需要三个部分:作为驱动的社区 SQLite 插件、具有明确列类型的实体,以及针对本地数据库生成的 sqljs 并作为类导入的迁移文件。配置完成后,Repository、关联关系和事务的处理方式与在后端时完全一致。
若想在以下环境进行更轻量级的配置, @capgo/capacitor-fast-sql请参阅 Capacitor 配合 Kysely。关于插件的基础用法,请从 如何在 Capacitor 应用中使用 SQLite 开始学习。当原生端发生变化时, Capgo 构建 可以在云端重新构建并签名 iOS 和 Android 应用。