Drizzle ORM berjalan di dalam sebuah Capacitor aplikasi melalui} sqlite-proxy driver: you give Drizzle one async function that executes SQL on a native SQLite plugin and returns rows as arrays, and Drizzle handles the typed query builder, relational queries and transactions on top. This guide builds that function for @capacitor-community/sqliteBerikut cara mengirimkan migrasi Kit Drizzle ke dalam bundle aplikasi, serta daftar kasus sampingan yang dapat menyebabkan hasil rusak jika tidak diatasi.
Semua hal di bawah ini telah diuji dengan drizzle-orm 0.45 dan drizzle-kit 0.31 pada Capacitor 8.
Mengapa menggunakan Drizzle pada database mobile
- Schema dalam TypeScript. Table adalah objek TypeScript biasa. Tipe untuk select dan insert diinfer, tidak ada code langkah pengembangan.
- SQL-berbentuk API.
db.select().from(users).where(eq(users.id, 1))Peta satu-satu ke SQL, sehingga Anda dapat berpikir tentang apa yang dijalankan di perangkat. - Migrasi yang dihasilkan dari schema. Kit Drizzle membandingkan schema Anda dan menulis file migrasi SQL.
- Tidak ada dekorator. Kerja dengan Vite dan esbuild tanpa flag TypeScript tambahan.
Perbandingan: Drizzle tidak memiliki pengemudi Capacitor di paketnya, jadi Anda harus menulis jembatan itu sendiri. Itu sekitar 30 baris.
Memilih plugin SQLite
Driver Drizzle sqlite-proxy mengatur hasil baris berdasarkan posisi: untuk all dan values itu mengharapkan array dari array, untuk get array tunggal, dalam urutan yang sama dengan kolom-kolom dalam yang dihasilkan SELECT. Capacitor SQLite plugins return objects keyed by column name, so the driver has to turn each object back into an array. That only works when the object keys keep the column order.
| Format baris | __CAPGO_KEEP_0__ | Urutan kolom dipertahankan | Kompatibel untuk proxy Drizzle |
|---|---|---|---|
@capacitor-community/sqlite |
Objek | Ya. Pada iOS plugin mengirimkan ios_columns daftar dan lapisan JS membangun kembali setiap baris dalam urutan kolom |
Sangat baik |
@capgo/capacitor-fast-sql |
Objek | Tidak dijamin pada iOS, baris dikirimkan dari sebuah kamus Swift | Gunakan Kysely |
Alasan itu panduan ini menggunakan plugin komunitas. Jika Anda sudah menggunakan Fast SQL, atau ingin jalur data yang lebih cepat, Kysely membaca baris berdasarkan nama kolom dan bekerja langsung dengan itu. Untuk penjelasan singkat tentang kedua plugin, lihat Cara Menggunakan SQLite di Aplikasi Capacitor.
Install
bun add drizzle-orm @capacitor-community/sqlite
bun add -d drizzle-kit
bunx cap sync
@capacitor-community/sqlite SQLCipher diinstal pada iOS dan Android, sehingga aplikasi Anda mengirimkan enkripsi code bahkan jika Anda tidak mengenkripsi apa-apa. Ingatlah hal ini untuk pertanyaan kompatibilitas App Store.
Mendefinisikan skema
// src/db/schema.ts
import { relations } from 'drizzle-orm';
import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
email: text('email').notNull().unique(),
createdAt: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
});
export const posts = sqliteTable(
'posts',
{
id: integer('id').primaryKey({ autoIncrement: true }),
authorId: integer('author_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
title: text('title').notNull(),
published: integer('published', { mode: 'boolean' }).notNull().default(false),
},
(t) => [index('idx_posts_author').on(t.authorId)],
);
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, { fields: [posts.authorId], references: [users.id] }),
}));
Beberapa detail penting pada perangkat mobile:
mode: 'timestamp'menyimpan detik sejak epoch sebagai bilangan bulat dan memberikan AndaDateobjek-objek dalam TypeScript.$defaultFn(() => new Date())menetapkan nilai default dalam JavaScript. Nilai default SQL sepertiunixepoch()memerlukan SQLite 3.38 atau lebih baru, yang tidak ada pada sistem Android lama SQLite.mode: 'boolean'menyimpan1/0Drizzle mengubah keduanya.relations()adalah apa yang menggerakkandb.query.*bersamawith: {...}. Hanya kunci asing saja tidak cukup untuk query relasional.
Open the connection
Plugin komunitas menjaga registri koneksi. Selama reload hidup atau penggantian modul panas, code Anda dapat menjalankan dua kali, jadi gunakan koneksi yang sudah ada daripada membuat yang kedua.
// src/db/connection.ts
import {
CapacitorSQLite,
SQLiteConnection,
type SQLiteDBConnection,
} from '@capacitor-community/sqlite';
const sqlite = new SQLiteConnection(CapacitorSQLite);
export async function openConnection(name: string): Promise<SQLiteDBConnection> {
const consistent = (await sqlite.checkConnectionsConsistency()).result;
const exists = (await sqlite.isConnection(name, false)).result;
const conn =
consistent && exists
? await sqlite.retrieveConnection(name, false)
: await sqlite.createConnection(name, false, 'no-encryption', 1, false);
await conn.open();
await conn.execute('PRAGMA foreign_keys = ON', false);
return conn;
}
Plugin menambahkan SQLite.db ke nama file, sehingga app menjadi appSQLite.db pada disk. Di web, plugin membutuhkan jeep-sqlite elemen kustom dan sqlite.initWebStore() sebelum koneksi pertama. Ikuti pengaturan web plugin jika Anda targetkan browser.
Driver sqlite-proxy
Inti dari integrasi ini. Drizzle memanggil fungsi dengan SQL, parameter, dan method:
run: tidak perlu baris apa pun (INSERT, UPDATE, DELETE, DDL, pengendalian transaksi).all: setiap baris, sebagai array.get: baris pertama, sebagai array, atauundefined.values: setiap baris, sebagai array (digunakan oleh raw)values()Mengapa setiap cabang ada:
// src/db/client.ts
import type { SQLiteDBConnection } from '@capacitor-community/sqlite';
import { drizzle } from 'drizzle-orm/sqlite-proxy';
import * as schema from './schema';
const READ = /^\s*(select|pragma|with|explain)\b/i;
const TX_CONTROL = /^\s*(begin|commit|rollback|savepoint|release)\b/i;
export function createDb(conn: SQLiteDBConnection) {
return drizzle(
async (sql, params, method) => {
// Transaction control: no params, no rows, no implicit wrapping
if (TX_CONTROL.test(sql)) {
await conn.execute(sql, false);
return { rows: [] };
}
let rows: Record<string, unknown>[];
if (READ.test(sql)) {
rows = (await conn.query(sql, params)).values ?? [];
} else {
// 'all' returns rows for INSERT/UPDATE/DELETE ... RETURNING
const res = await conn.run(sql, params, false, method === 'run' ? 'no' : 'all');
rows = res.changes?.values ?? [];
}
if (method === 'run') return { rows: [] };
// Drizzle maps by position. The plugin keeps column order in each object.
const arrays = rows.map((row) => Object.values(row));
return { rows: method === 'get' ? (arrays[0] as any) : arrays };
},
{ schema },
);
}
export type AppDb = ReturnType<typeof createDb>;
: Drizzle menerapkan
TX_CONTROLdengan mengirimkandb.transaction(), danbegin,commit,rollback, andsavepoint/releaseUntuk transaksi yang terikat. Mereka melaluiexecute(sql, false), the same path TypeORM’s official Capacitor driver uses forBEGIN/COMMIT.falsesebagai argumen ketigarun(): Pengaturan default plugin ini mengemas setiap statement dalam transaksi sendiri, yang bertabrakan dengan Drizzle’s explicitbegin.returnMode 'all': membuat.returning()berfungsi untuk insert, update, dan delete.Object.values(row): mengubah baris yang dikunci kembali menjadi array posisi.
Saya menjalankan fungsi ini secara eksak terhadap SQLite yang berjalan di memori dan mengembalikan baris seperti yang dilakukan plugin ini. Insert dengan .returning(), select yang disaring .get(), query relasional, transaksi, dan rollback pada kesalahan yang dilempar semua menghasilkan hasil yang benar.
Diinisialisasi sekali saja
// src/db/index.ts
import { openConnection } from './connection';
import { createDb, type AppDb } from './client';
import { runMigrations } from './migrate';
import bundle from '../../drizzle/migrations';
let dbPromise: Promise<AppDb> | null = null;
export function getDb(): Promise<AppDb> {
if (!dbPromise) {
dbPromise = (async () => {
const conn = await openConnection('app');
await runMigrations(conn, bundle);
return createDb(conn);
})();
}
return dbPromise;
}
Migrasi dengan Drizzle Kit
migrator bawaan Drizzle membaca sqlite-proxy file dari disk dengan Node’s .sql berkas dari disk dengan Node's fsdrizzle yang di mana WebView tidak memiliki. Solusi adalah membiarkan Drizzle Kit mengemas SQL ke dalam modul JavaScript dan menerapkannya sendiri.
1. Konfigurasi Drizzle Kit
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'sqlite',
driver: 'expo',
});
driver: 'expo' tidak berarti Anda memerlukan Expo. Ini menginstruksikan Drizzle Kit untuk juga menulis drizzle/migrations.jsyang mengimport jurnal dan setiap .sql file. Itu adalah format yang dibutuhkan oleh runtime non-Node.
2. Generasi
bunx drizzle-kit generate
You get drizzle/0000_<name>.sql, drizzle/meta/_journal.json dan drizzle/migrations.jsCommit semua tiga. Jalankan generate Jangan edit file SQL yang sudah dikirimkan lagi setelah setiap perubahan schema.
3. Biarkan bundler mengimport .sql sebagai teks
migrations.js memiliki baris seperti import m0000 from './0000_fearless_selene.sql'Vite tidak tahu. .sqlJadi tambahkan plugin kecil daripada dependensi lain:
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
{
name: 'sql-as-text',
transform(code, id) {
if (id.endsWith('.sql')) {
return { code: `export default ${JSON.stringify(code)};`, map: null };
}
},
},
],
});
Dan deklarasi jenis untuk TypeScript agar menerima import:
// src/sql.d.ts
declare module '*.sql' {
const content: string;
export default content;
}
Dengan webpack (pengembang kustom Angular, konfigurasi lama) gunakan aturan untuk asset/source file-file .sql 4. Lakukan migrasi pada startup
Aplikasikan migrasi pada awal startup
// src/db/migrate.ts
import type { SQLiteDBConnection } from '@capacitor-community/sqlite';
interface MigrationBundle {
journal: { entries: { idx: number; when: number; tag: string }[] };
migrations: Record<string, string>;
}
export async function runMigrations(conn: SQLiteDBConnection, bundle: MigrationBundle) {
await conn.execute(
`CREATE TABLE IF NOT EXISTS __drizzle_migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hash TEXT NOT NULL,
created_at NUMERIC
)`,
false,
);
const res = await conn.query(
'SELECT created_at FROM __drizzle_migrations ORDER BY created_at DESC LIMIT 1',
);
const lastApplied = Number(res.values?.[0]?.created_at ?? 0);
for (const entry of bundle.journal.entries) {
if (entry.when <= lastApplied) continue;
const key = `m${entry.idx.toString().padStart(4, '0')}`;
const sqlText = bundle.migrations[key];
if (!sqlText) throw new Error(`Missing migration ${entry.tag}`);
const statements = sqlText
.split('--> statement-breakpoint')
.map((s) => s.trim())
.filter(Boolean);
await conn.beginTransaction();
try {
for (const statement of statements) {
await conn.execute(statement, false);
}
await conn.run(
'INSERT INTO __drizzle_migrations (hash, created_at) VALUES (?, ?)',
[entry.tag, entry.when],
false,
);
await conn.commitTransaction();
} catch (error) {
await conn.rollbackTransaction();
throw error;
}
}
}
This follows the same rule Drizzle’s own migrators use: a migration is pending when its journal timestamp is newer than the newest one recorded. Each migration runs in its own transaction, so a failure leaves the previous state intact. Calling it on every launch is cheap.
__CAPGO_KEEP_0__ pembaruan hidup Capgo pembaruan langsung, new migrations reach users without a store release. Keep them additive (new tables, new nullable columns), because a rollback to an older bundle will run against the newer schema.
Query
import { and, desc, eq, sql } from 'drizzle-orm';
import { getDb } from './db';
import { posts, users } from './db/schema';
const db = await getDb();
// Insert and get the row back
const [ada] = await db
.insert(users)
.values({ name: 'Ada', email: 'ada@example.com' })
.returning();
// Bulk insert
await db.insert(posts).values([
{ authorId: ada.id, title: 'Draft' },
{ authorId: ada.id, title: 'Live', published: true },
]);
// Select with filters
const live = await db
.select()
.from(posts)
.where(and(eq(posts.authorId, ada.id), eq(posts.published, true)))
.orderBy(desc(posts.id));
// Single row or undefined
const user = await db.select().from(users).where(eq(users.email, 'ada@example.com')).get();
// Update and delete
await db.update(posts).set({ published: true }).where(eq(posts.id, live[0].id));
await db.delete(posts).where(eq(posts.published, false));
// Aggregate
const [{ count }] = await db
.select({ count: sql<number>`count(*)` })
.from(posts);
Kueri Relasional
const authors = await db.query.users.findMany({
with: { posts: { where: (p, { eq }) => eq(p.published, true) } },
});
const one = await db.query.users.findFirst({
where: (u, { eq }) => eq(u.id, ada.id),
with: { posts: true },
});
Drizzle membangun ini dengan menggunakan fungsi JSON SQLite, kemudian menerjemahkan hasilnya. Mereka berfungsi di iOS dan di versi Android terbaru. Jika Anda minSdkVersion perlu diperbarui, cek SELECT json('{}') Koneksi: nama kolom duplikat alias
Joins: nama kolom alias duplikat
ini adalah satu sisi tajam dari setiap driver proxy yang dibangun di atas baris objek. Drizzle tidak menambahkan AS alias ke kolom-kolom yang digabung, jadi query ini:
// Wrong: both columns are named "id" in the result set
await db
.select({ postId: posts.id, userId: users.id })
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id));
mengembalikan id pengguna dan id. They collapse into one key in the row object, the array is one value short, and Drizzle assigns values to the wrong fields. In my test postId . Tidak ada kesalahan yang ditimbulkan. userId was undefinedKueri ini menghasilkan dua kolom yang disebut
Alias mereka secara eksplisit:
await db
.select({
postId: sql<number>`${posts.id}`.as('post_id'),
userId: sql<number>`${users.id}`.as('user_id'),
title: posts.title,
author: users.name,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id));
Pemilihan kolom dengan nama yang berbeda (posts.title, users.name) memang baik. Hindari db.select().from(a).innerJoin(b, ...) tanpa daftar bidang ketika kedua tabel memiliki nama kolom yang sama seperti id atau created_atKueri relasional tidak memiliki masalah ini.
Transactions
await db.transaction(async (tx) => {
const [user] = await tx
.insert(users)
.values({ name: 'Grace', email: 'grace@example.com' })
.returning();
await tx.insert(posts).values({ authorId: user.id, title: 'Hello' });
// Nested transactions become savepoints
await tx.transaction(async (inner) => {
await inner.insert(posts).values({ authorId: user.id, title: 'Maybe' });
});
});
Pertanyaan Alternatif: Apakah Anda ingin menggunakan rollback. Calling tx.rollback() Pertanyaan Alternatif: Apakah Anda ingin menggunakan
Jangan membuat transaksi yang terlalu panjang karena ada satu koneksi per database. await Panggilan jaringan di dalamnya. Penulisan lain pada koneksi yang sama akan berjalan di dalam transaksi terbuka Anda.
Troubleshooting
Error 'Koneksi sudah ada' saat startup. Inisialisasi Anda berjalan dua kali, biasanya karena hot reload. Gunakan checkConnectionsConsistency and retrieveConnection Polanya di atas.
cannot start a transaction within a transaction. A run() Tidak ada panggilan yang dibuat false sebagai argumen ketiga, sehingga plugin membuka transaksinya sendiri di dalam Drizzle.
Migrasi dijalankan pada setiap peluncuran dan gagal dengan table already exists. The __drizzle_migrations insert gagal atau dibatalkan. Periksa bahwa jurnal when Nilai-nilai di dalam bundle Anda sesuai dengan baris-baris di dalam tabel.
Error Pembangunan Failed to parse source for import analysis di .sql. Plugin Vite transform hilang atau berjalan setelah plugin lain yang mencoba menerjemahkan file. Letakkan di atasnya. plugins array.
.returning() kembali array kosong. dengan mode kembali run Jamin driver melewati noPastikan driver dapat melewati 'all' bukan method tidak ada run.
Tgl-tgl salah dengan faktor 1000. mode: 'timestamp' menyimpan detik, mode: 'timestamp_ms' simpan milisecond. Pilih satu per kolom dan jangan campur dengan data mentah Date.now() menyisipkan.
Mengapa memilih sesuatu lain
- Anda ingin jalur data tercepat dengan Fast SQL, atau objek baris yang dikunci dengan nama: Kysely dengan Capacitor.
- Anda lebih suka dekorator, entitas, dan repositori, atau Anda sedang mengimpor model backend TypeORM: TypeORM dengan Capacitor.
- Anda membutuhkan enkripsi pada ruang penyimpanan: Mengenkripsi database SQLite di Capacitor.
Menggunakan plugin asli berarti harus membangun aplikasi iOS dan Android kembali. Jika Anda tidak memiliki Mac di dekat Anda: Capgo Bangun membangun dan menandatangani baik di awan.