TypeORM 在 Capacitor 应用中通过其内置的 capacitor driver,它会在……上执行每个查询 @capacitor-community/sqlite。你配置一个 DataSource ,使用插件的 SQLiteConnection,用装饰器定义实体,并像在服务端一样使用 repository 和事务。本指南涵盖 TypeORM 1.x 和 Capacitor 8 的完整设置,包括大多数教程忽略的两点:Vite 的显式列类型,以及如何在 CLI 无法与手机通信的情况下生成迁移。
TypeORM 适用场景
TypeORM 是一个数据映射器 ORM,包含实体、关系、repository 和迁移系统。在移动端,以下情况适合使用:
- 后端已使用 TypeORM,希望应用中也采用相同的实体风格,
- 数据模型包含大量关系,且你希望
relations: { posts: true }而不是手写 join, - 你的团队更倾向于使用类和装饰器,而非 schema 对象。
它比 query builder 更重。如果 bundle 大小和启动时间最关键,或者你想使用 @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 的实体
大多数 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 可能会删除列以匹配实体,从而清除设备上的用户数据。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' 它会将每个迁移都包在各自的事务中,因此一旦失败,之前的迁移仍保持已应用,失败的迁移则会回滚。
Repositories: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 帮你省时间的地方。用 .getSql() 在开发时检查生成的 SQL,并为 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 或另一个 repository 在事务打开期间执行的查询,会在同一连接上运行,并随事务一起提交或回滚。在事务结束前不要启动无关的数据库操作,保持事务简短,且绝不要在事务内等待网络请求。
迁移:使用本地 sqljs DataSource 生成迁移文件
TypeORM 的 CLI 运行在您的电脑上,而不是手机上,因此无法检查设备数据库。该插件的官方文档指出,capacitor 类型的迁移必须手动编写。您仍然可以生成迁移: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. 将 scratch 数据库更新到最新,然后 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 数据源和实体。第一个命令将现有迁移应用到临时数据库。第二个命令将其与你的实体对比,并写入新的迁移类,例如 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>创建新表、复制行、删除原表并重命名。这本身没错,但在手机上处理大表会很耗时,所以运行时请加上 loading 状态。
手动编写迁移
如果你自己编写迁移,请让每条语句都以大写关键字开头。capacitor query runner 会区分大小写地查看第一个词,以决定如何调用插件: BEGIN, COMMIT, ROLLBACK, CREATE, ALTER 以及 DROP 转到 execute, INSERT, UPDATE 以及 DELETE 转到 run,其他所有内容都会转到 query。小写的 create table 会被作为查询发送,这不是 DDL 想要的结果。
迁移与 Live Updates
Migration 类是 JavaScript bundle 的一部分。配合 Capgo 实时更新 schema 变更无需经过应用商店审核即可触达用户。回滚可能会把旧版 code 放到较新的 schema 上,因此建议优先采用增量迁移(新增表、新增可空列),并在后续版本中再删除旧列。
Web 端支持
该社区插件在 Web 端通过 jeep-sqlite web component(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。
迁移在 CLI 中执行了,但未在设备上执行。 新类不在 migrations 数组中的 data-source.ts。应用不会扫描文件夹。
总结
在 Capacitor 上使用 TypeORM 分三部分:作为驱动的社区 SQLite 插件、带显式列类型的实体,以及针对本地 sqljs 数据库生成并以类导入的迁移文件。配置好后,Repository、关联和事务的行为与后端一致。
更轻量的方案是 @capgo/capacitor-fast-sql,请参阅 Kysely 搭配 Capacitor。插件基础用法请从 如何在 Capacitor 应用中使用 SQLite。原生端有改动时, Capgo 构建 可在云端重新构建并签名 iOS 和 Android 应用。