KyselyはCapacitorとSQLiteとよく組み合わされます。なぜなら、プラットフォームからただ1つのものしか必要としません:ドライバーがSQL文字列とパラメータを送信し、オブジェクトとしての行を返します。 @capgo/capacitor-fast-sqlこのガイドでは、そのドライバーを構築し、iOS、Android、Web上で実行されるアプリ内で実行されるTypedテーブル、CRUD、JOIN、トランザクション、ミグレーションについて説明します。
コンパイル時チェックされたSQLをcode生成なしで、重いランタイムなしで取得します。
なぜKyselyはモバイルSQLiteに適しているか
Kyselyはクエリビルダーであり、ORMではありません。SQLのようなものを書き、TypeScriptはすべてのテーブルと列名をチェックし、結果の型を推測します。
- デコレータやリフレクションは必要ありません。Viteとesbuildを箱から出して動作します。
- code生成ステップはありません。
Databaseインターフェイスはスキーマ契約です。 - 小さなランタイムがよく削除できるため、実行可能ファイルサイズとダウンロードサイズに影響します。 live update SQLiteをアプリに設定していない場合は、まず
- モバイルアプリでSQLiteを使用する方法
を参照してください。このドキュメントでは、Fast SQLが必要とするネイティブの構成をカバーしています。 SQLiteをCapacitorアプリに使う方法KyselyはSQLiteのクエリを実行するための最小限のドライバインターフェイスを提供します。NSAllowsLocalNetworking iOSとlocalhostの明示ルール (iOSとlocalhostの明示ルール)
インストール
bun add kysely @capgo/capacitor-fast-sql
bunx cap sync
このガイドではKysely 0.29とFast SQL 8.2を使用しています。 Kysely 0.29では、移行クラスはエントリポイントに移動しました。 kysely/migration エントリ ポイント
Fast SQL 方言を書く
Kysely Dialect Kyselyには4つのファクトリがあります。3つはKyselyのSQLiteサポートから直接来ています。4つ目はドライバーです。
// src/db/fast-sql-dialect.ts
import {
CompiledQuery,
SqliteAdapter,
SqliteIntrospector,
SqliteQueryCompiler,
type DatabaseConnection,
type DatabaseIntrospector,
type Dialect,
type DialectAdapter,
type Driver,
type Kysely,
type QueryCompiler,
type QueryResult,
} from 'kysely';
import {
FastSQL,
type SQLConnection,
type SQLConnectionOptions,
type SQLValue,
} from '@capgo/capacitor-fast-sql';
// Kysely passes JS values as-is. Normalize them to what SQLite stores.
function toSqlValue(value: unknown): SQLValue {
if (value === undefined || value === null) return null;
if (typeof value === 'boolean') return value ? 1 : 0;
if (value instanceof Date) return value.toISOString();
if (typeof value === 'bigint') return Number(value);
if (value instanceof Uint8Array) return value;
if (typeof value === 'object') return JSON.stringify(value);
return value as SQLValue;
}
class FastSqlConnection implements DatabaseConnection {
constructor(readonly db: SQLConnection) {}
async executeQuery<R>(compiled: CompiledQuery): Promise<QueryResult<R>> {
const result = await this.db.execute(compiled.sql, compiled.parameters.map(toSqlValue));
return {
rows: result.rows as R[],
numAffectedRows: result.rowsAffected != null ? BigInt(result.rowsAffected) : undefined,
insertId: result.insertId != null ? BigInt(result.insertId) : undefined,
};
}
// eslint-disable-next-line require-yield
async *streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
throw new Error('Streaming is not supported by the Fast SQL driver');
}
}
// One native connection per database, so serialize access to it.
class ConnectionMutex {
#pending: Promise<void> = Promise.resolve();
#release?: () => void;
async lock(): Promise<void> {
const previous = this.#pending;
let release!: () => void;
this.#pending = new Promise<void>((resolve) => (release = resolve));
await previous;
this.#release = release;
}
unlock(): void {
const release = this.#release;
this.#release = undefined;
release?.();
}
}
class FastSqlDriver implements Driver {
#connection?: FastSqlConnection;
readonly #mutex = new ConnectionMutex();
constructor(private readonly options: SQLConnectionOptions) {}
async init(): Promise<void> {
const db = await FastSQL.connect(this.options);
this.#connection = new FastSqlConnection(db);
}
async acquireConnection(): Promise<DatabaseConnection> {
await this.#mutex.lock();
return this.#connection!;
}
async releaseConnection(): Promise<void> {
this.#mutex.unlock();
}
async beginTransaction(connection: DatabaseConnection): Promise<void> {
await (connection as FastSqlConnection).db.beginTransaction();
}
async commitTransaction(connection: DatabaseConnection): Promise<void> {
await (connection as FastSqlConnection).db.commit();
}
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
await (connection as FastSqlConnection).db.rollback();
}
async destroy(): Promise<void> {
if (this.#connection) {
await FastSQL.disconnect(this.options.database);
this.#connection = undefined;
}
}
}
export class FastSqlDialect implements Dialect {
constructor(private readonly options: SQLConnectionOptions) {}
createDriver(): Driver {
return new FastSqlDriver(this.options);
}
createQueryCompiler(): QueryCompiler {
return new SqliteQueryCompiler();
}
createAdapter(): DialectAdapter {
return new SqliteAdapter();
}
createIntrospector(db: Kysely<any>): DatabaseIntrospector {
return new SqliteIntrospector(db);
}
}
各部分の役割
toSqlValue真値を1/0日付をISO形式の文字列に、平文オブジェクトをJSONに変換します。SQLiteには真値または日付の型がありません。Androidプラグインでは、読み取りクエリのパラメータをテキストとしてバインドするため、trueテキストとして到着し、'true'マッチしない1.FastSqlConnection.executeQueryコールexecute()iOS と Android ではプラグインのローカル HTTP チャネル、ウェブでは SQLite Wasm を通じて、Fast SQL 接続にアクセスします。既存の行は、列名でキー化されたオブジェクトとして返されます。これは、Kysely が期待するものと同じです。ConnectionMutexFast SQL では、1 つのデータベースあたり 1 つのネイティブ接続を維持します。ロックがなければ、2 つの並行 Kysely トランザクションは、1 つの接続にそれぞれのステートメントを交互に実行します。Kysely の SQLite ドライバーも同様のパターンを使用しています。- トランザクション プラグインのネイティブ
beginTransaction,commitとrollback代わりに、raw SQL の代わりに、プラグインは両方のプラットフォームでトランザクションの状態を正しく追跡します。beginセーブポイント - Savepoints を投げます。
savepoint()追加The savepoint method is not supported by this driver. Addsavepoint,rollbackToSavepointJapanesereleaseSavepoint/ja/blog/kysely-capacitor-sqlite/
Live Update
Cloudflare Database Capacitor
// src/db/schema.ts
import type { ColumnType, Generated, Insertable, Selectable, Updateable } from 'kysely';
export interface ProjectTable {
id: Generated<number>;
name: string;
created_at: ColumnType<string, string | undefined, never>;
}
export interface TaskTable {
id: Generated<number>;
project_id: number;
title: string;
done: ColumnType<number, number | boolean | undefined, number | boolean>;
due_at: string | null;
}
export interface Database {
projects: ProjectTable;
tasks: TaskTable;
}
export type Task = Selectable<TaskTable>;
export type NewTask = Insertable<TaskTable>;
export type TaskUpdate = Updateable<TaskTable>;
Generated<number> GitHub id Capgo ColumnType<Select, Insert, Update> code done API 1/0. never SDK
CLI
// src/db/index.ts
import { Kysely } from 'kysely';
import { FastSqlDialect } from './fast-sql-dialect';
import type { Database } from './schema';
import { runMigrations } from './migrations';
let instance: Promise<Kysely<Database>> | null = null;
export function getDb(): Promise<Kysely<Database>> {
if (!instance) {
instance = (async () => {
const db = new Kysely<Database>({
dialect: new FastSqlDialect({
database: 'tasks',
walMode: true,
performancePresets: true, // also enables foreign_keys
}),
});
await runMigrations(db);
return db;
})();
}
return instance;
}
npm init() 最初のクエリで遅延実行されるため、インスタンスの構築は安価です。
アプリ内でのマイグレーション
Kyselyの FileMigrationProvider Nodeの fsWebViewでは存在しません。codeでマイグレーションを提供するのを検討してください。 Migrator マイグレーションを適用した名前はテーブルに格納され、順序に従って実行される kysely_migration 注記:
// src/db/migrations.ts
import { sql, type Kysely } from 'kysely';
import { Migrator, type Migration, type MigrationProvider } from 'kysely/migration';
const migrations: Record<string, Migration> = {
'0001_init': {
async up(db: Kysely<any>) {
await db.schema
.createTable('projects')
.addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
.addColumn('name', 'text', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`))
.execute();
await db.schema
.createTable('tasks')
.addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
.addColumn('project_id', 'integer', (col) =>
col.notNull().references('projects.id').onDelete('cascade'),
)
.addColumn('title', 'text', (col) => col.notNull())
.addColumn('done', 'integer', (col) => col.notNull().defaultTo(0))
.execute();
},
},
'0002_due_date': {
async up(db: Kysely<any>) {
await db.schema.alterTable('tasks').addColumn('due_at', 'text').execute();
await db.schema.createIndex('idx_tasks_project').on('tasks').column('project_id').execute();
},
},
};
class InCodeMigrationProvider implements MigrationProvider {
async getMigrations(): Promise<Record<string, Migration>> {
return migrations;
}
}
export async function runMigrations(db: Kysely<any>): Promise<void> {
const migrator = new Migrator({ db, provider: new InCodeMigrationProvider() });
const { error, results } = await migrator.migrateToLatest();
results?.forEach((r) => {
if (r.status === 'Error') console.error(`Migration ${r.migrationName} failed`);
});
if (error) throw error;
}
Notes:
- スキーマの半分がマイグレーションされていても、エラーを投げるのではなく
migrateToLatest()結果を返します。自分で確認するか、またはアプリが半分のスキーマで開始することを確認してください。{ error, results }, したので確認してください。errorMigrations inside the app- 起動ごとに呼び出すのは安い。 pendding がなければ、 マイグレーションテーブルを読み取って返す。
- SQLite はトランザクション的DDLをサポートしているので、Migrator はrunをトランザクション内に包む。 上記のドライバーと一緒に機能する。
- マイグレーションを
Kysely<any>、現在のDatabaseのタイプ。 既存のマイグレーションはスキーマが進化してもコンパイルできるようにする必要がある。
クエリ
インサート
const db = await getDb();
const result = await db
.insertInto('projects')
.values({ name: 'Groceries' })
.executeTakeFirst();
const projectId = Number(result.insertId);
await db
.insertInto('tasks')
.values([
{ project_id: projectId, title: 'Milk' },
{ project_id: projectId, title: 'Bread', done: true },
])
.execute();
insertId は bigint の Number() Kysely ではインサートの代わりに使用する。
Avoid .returning() 避ける。 Android では現在のFast SQLバージョンをターゲットにしている場合、Androidは行が開始するステートメントで始まるステートメントのみを返すためです。 SELECT, PRAGMA または EXPLAIN、 INSERT ... RETURNING 実行されますが、結果はありません。iOSとWebは、行を返します。読み insertId どこでも機能します。
選択
const openTasks = await db
.selectFrom('tasks')
.select(['id', 'title', 'due_at'])
.where('project_id', '=', projectId)
.where('done', '=', 0)
.orderBy('id')
.execute();
// openTasks: { id: number; title: string; due_at: string | null }[]
const project = await db
.selectFrom('projects')
.selectAll()
.where('id', '=', projectId)
.executeTakeFirst(); // undefined if not found
結合と集計
const summary = await db
.selectFrom('projects as p')
.leftJoin('tasks as t', 't.project_id', 'p.id')
.select((eb) => [
'p.id',
'p.name',
eb.fn.count<number>('t.id').as('task_count'),
eb.fn.sum<number>('t.done').as('done_count'),
])
.groupBy('p.id')
.execute();
Kyselyは結合後、存在する列を認識し、エイリアス(task_count, done_count)は結果の型に含まれます。行がオブジェクトとして返されるため、選択した各列に一意の名前を付けます。2つの列が両方とも id という名前の場合、行オブジェクトでは互いに上書きされます。これは、SQLiteドライバーがオブジェクトを返す場合に真です。
ネストされた結果とJSONヘルパー
import { jsonArrayFrom } from 'kysely/helpers/sqlite';
const projects = await db
.selectFrom('projects')
.select((eb) => [
'projects.id',
'projects.name',
jsonArrayFrom(
eb.selectFrom('tasks')
.select(['tasks.id', 'tasks.title'])
.whereRef('tasks.project_id', '=', 'projects.id'),
).as('tasks'),
])
.execute();
SQLiteのJSON関数に依存しています。iOSでは組み込まれていますが、Webビルドと最近のAndroidリリースでは組み込まれていませんが、古い minSdkVersion は古いです。 SELECT json('{}') あなたの最古のテストデバイスから始めてください。
アップデートと削除
await db.updateTable('tasks').set({ done: true }).where('id', '=', taskId).execute();
const deleted = await db.deleteFrom('tasks').where('done', '=', 1).executeTakeFirst();
console.log(Number(deleted.numDeletedRows));
トランザクション
await db.transaction().execute(async (trx) => {
const { insertId } = await trx
.insertInto('projects')
.values({ name: 'Trip' })
.executeTakeFirstOrThrow();
await trx
.insertInto('tasks')
.values(['Passport', 'Tickets', 'Charger'].map((title) => ({
project_id: Number(insertId),
title,
})))
.execute();
});
コールバックが投げる場合、ドライバーはロールバックします。使用 trx、 db、コールバック内 db 待機するクエリは、トランザクションが保持しているミューテックスを待ちます。したがって、トランザクションが終了するまでデッドロックになります。
必要な場合にのみ使用するRaw SQL
import { sql } from 'kysely';
const { rows } = await sql<{ journal_mode: string }>`PRAGMA journal_mode`.execute(db);
The sql 信頼できるフラグメントのみに使用してください。 sql.raw() コミュニティプラグインを使用する代わりに
SQLを直接実行する必要がある場合
もしあなたがCapacitorのSQLiteを使用している場合 @capacitor-community/sqlite, 方言、アダプター、コンパイラ、ミューテックスを維持し、接続を置き換えます。
import type { SQLiteDBConnection } from '@capacitor-community/sqlite';
class CommunityConnection implements DatabaseConnection {
constructor(readonly db: SQLiteDBConnection) {}
async executeQuery<R>(compiled: CompiledQuery): Promise<QueryResult<R>> {
const params = compiled.parameters.map(toSqlValue);
if (/^\s*(select|pragma|with)\b/i.test(compiled.sql)) {
const res = await this.db.query(compiled.sql, params);
return { rows: (res.values ?? []) as R[] };
}
const res = await this.db.run(compiled.sql, params, false, 'all');
return {
rows: (res.changes?.values ?? []) as R[],
numAffectedRows: BigInt(res.changes?.changes ?? 0),
insertId: res.changes?.lastId != null ? BigInt(res.changes.lastId) : undefined,
};
}
async *streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
throw new Error('Streaming is not supported');
}
}
Pass false __CAPGO_KEEP_0__を第三引数として run()Capgoのプラグインの設定を変更してください。 beginTransaction(), commitTransaction() and rollbackTransaction() context: Capgo marketing website. Role: Short UI label or navigation item. Seen in: page trust.astro. Message key `and` (And).
in the driver.
Cannot find module 'kysely/migration'. Troubleshooting Migrator context: Support / premium support page or footer support section. Role: Section or page heading. Seen in: page support-policy.astro. Message key `support_policy_troubleshooting_title` (Support Policy Troubleshooting Title). kysely You are on Kysely older than 0.29, where
is exported from Capgo db Capacitor db.transaction().execute()SQLite trx Capacitor
done = true Capacitor Capacitor toSqlValueCapacitor sql Capacitor
no such table: kysely_migration Capacitor Capacitor init() Capacitor
Capacitor 日本語 Database /ja/blog/kysely-capacitor-sqlite/
Live Update
Kyselyは約80行のglueが必要です。Capacitorに接続するためのコネクタが必要です。 execute()Capacitor
GitHub Drizzle ORM with Capacitor code TypeORMとCapacitor SDK CLI npm