Drizzle ORM은 Capacitor 앱 내에서 작동합니다. sqlite-proxy __CAPGO_KEEP_0__ 앱 내에서 Drizzle ORM이 작동하는 드라이버를 통해 작동합니다. 드라이버는 Drizzle에게 SQL을 실행하는 데 사용할 수 있는 하나의 비동기 함수를 제공하고, SQLite 플러그인에 대한 결과를 배열로 반환합니다. Drizzle은 타입화된 쿼리 빌더, 관계 쿼리 및 트랜잭션을 처리합니다. @capacitor-community/sqlite이 가이드는 이러한 함수를 빌드하고, Drizzle Kit 마이그레이션을 앱 배포에 포함하는 방법을 보여주며, 이러한 마이그레이션을 놓치면 결과가 손상되는 Edge 케이스를 나열합니다.
모든 아래는 __CAPGO_KEEP_0__ 8에서 0.45와 0.31에 테스트되었습니다. drizzle-orm 0.45와 0.31 drizzle-kit 0.31 버전은 Capacitor 8 버전입니다.
모바일 데이터베이스에서 Drizzle를 사용하는 이유
- TypeScript에서 스키마 테이블은 평범한 TypeScript 객체입니다. 선택과 삽입에 대한 타입은 추론되며, code 생성 단계가 필요하지 않습니다.
- 선택과 삽입에 대한 타입은 추론되며, API 생성 단계가 필요하지 않습니다.
db.select().from(users).where(eq(users.id, 1))SQL-형태의 __CAPGO_KEEP_0__ - SQL에 1:1로 맵핑되므로, 장치에서 실행되는 것을 추론할 수 있습니다. 스키마에서 마이그레이션을 생성합니다.
- Drizzle Kit은 스키마의 차이점을 감지하고 SQL 마이그레이션 파일을 작성합니다. 데코레이터가 없습니다.
Drizzle의 조건: Drizzle에는 패키지에 Capacitor 드라이버가 없기 때문에 bridge를 직접 작성해야 합니다. 그것은 약 30줄입니다.
SQLite 플러그인 선택
Drizzle의 sqlite-proxy 드라이버는 결과 행을 위치에 따라 매핑합니다 :그리고 all and values 단일 배열의 경우, 열을 생성한 get 한 개의 배열, 열이 생성된 순서와 동일한 순서로. 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.
| 행 형식 | 드라이버 | 열 순서 유지 | Drizzle 프록시를 위한 적합 |
|---|---|---|---|
@capacitor-community/sqlite |
객체 | iOS에서 플러그인은 열 순서에 따라 ios_columns 리스트를 전송하고 JS layer는 각 행을 열 순서에 따라 재구성합니다. |
좋음 |
@capgo/capacitor-fast-sql |
객체 | iOS에서 보장되지 않음, 행은 Swift 사전에서 직렬화됩니다. | Kysely를 사용하세요 |
이 가이드는 커뮤니티 플러그인을 사용하기 때문에 이와 같은 이유입니다. 이미 Fast SQL을 사용중이거나, 더 빠른 데이터 경로를 원하신다면, Kysely 열 이름에 따라 행을 읽고 직접 작업합니다. 두 플러그인에 대한 개요를 보려면 SQLite를 Capacitor 앱에서 어떻게 사용하는가?.
설치
bun add drizzle-orm @capacitor-community/sqlite
bun add -d drizzle-kit
bunx cap sync
@capacitor-community/sqlite code iOS 및 Android에서 SQLCipher를 패키징하여 앱이 암호화되지 않은 경우에도 암호화 code를 포함합니다. 앱 스토어 수출 준수에 대한 질문에 대해 이를 기억하세요.
스키마 정의
// 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] }),
}));
모바일에서 몇 가지 세부 사항이 중요합니다:
mode: 'timestamp'초기화 시점부터 현재까지의 초를 정수 형태로 저장하고 TypeScript에서 __CAPGO_KEEP_0__을 반환합니다.DateJavaScript에서 기본값을 설정합니다. SQL 기본값처럼 __CAPGO_KEEP_0__은 SQLite 3.38 이상이 필요하며, 이보다 이전의 Android 시스템 SQLite에는 없습니다.$defaultFn(() => new Date())__CAPGO_KEEP_0__unixepoch(). Drizzle는 양방향으로 변환합니다.mode: 'boolean'stores1/0__CAPGO_KEEP_0__relations()__CAPGO_KEEP_0__db.query.*withwith: {...}. 외래 키만으로 관계 쿼리를 충분히 처리할 수 없습니다.
연결을 열기
커뮤니티 플러그인은 연결의 레지스트리를 유지합니다. 라이브 리로드 또는 핫 모듈 교체 중에 code이 두 번 실행될 수 있으므로 기존 연결을 재사용하는 것이 두 번째 연결을 생성하는 것보다 좋습니다.
// 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;
}
The plugin appends __CAPGO_KEEP_0__ to the database name. SQLite.db 파일 이름에 추가되므로 app 파일 이름이 appSQLite.db 디스크에 저장됩니다. 웹에서 플러그인은 jeep-sqlite 커스텀 요소와 sqlite.initWebStore() 첫 번째 연결 전에 필요합니다. 브라우저를 대상으로 하는 경우 플러그인의 웹 설정을 따르세요.
sqlite-proxy 드라이버
이것이 통합의 핵심입니다. 드릴은 SQL, 매개 변수, 그리고 method:
runINSERT, UPDATE, DELETE, DDL, 트랜잭션 제어를 위한 행이 필요하지 않습니다.all각 행은 배열로 표시됩니다.get: 첫 번째 행, 배열, 또는undefined.values각 행, 배열로 사용 (raw에 의해 사용됨)values()SQLite와 Capacitor를 사용하는 Drizzle ORM
// 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>;
각 branch의 존재 이유:
TX_CONTROL드릴라이즈는 구현합니다db.transaction()by sendingbegin,commit,rollback, 및savepoint/releasenested 트랜잭션에 대해. 그들은 통과합니다.execute(sql, false)TypeORM의 공식 Capacitor 드라이버가 사용하는 동일한 경로BEGIN/COMMIT.falseas the third argument torun(): plugin 기본 설정은 각 statement를 자신의 transaction에 wrapping합니다. 이는 Drizzle의 explicit transaction과 충돌합니다.begin.returnMode 'all': makes.returning(): 키드된 row를 다시 위치 array로 변환합니다.Object.values(row): 키(row)의 위치를 다시 배열로 변환합니다.
: 한번만 초기화합니다. .returning(): Drizzle Kit의 .get(): Drizzle의 내장
: Node의
// 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;
}
migrator는 disk에서 파일을 읽습니다.
files sqlite-proxy from .sql disk fs, WebView가 가지고 있지 않은 기능입니다. 해결 방법은 Drizzle Kit이 SQL을 JavaScript 모듈로 패키징하고, 그 후 직접 적용하는 것입니다.
1. 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' Expo가 필요하지 않다는 것은 Drizzle Kit이 또한 SQL을 기록하도록 지시한다는 것을 의미합니다. drizzle/migrations.js, 이메일과 각 .sql 파일 형식입니다. Node 런타임 이외의 모든 런타임이 필요합니다.
2. 생성
bunx drizzle-kit generate
You receive drizzle/0000_<name>.sql, drizzle/meta/_journal.json 또한 drizzle/migrations.jscontext: Capgo 마케팅 웹사이트. 역할: 짧은 UI 레이블 또는 네비게이션 아이템. 페이지: trust.astro. 메시지 키 `and` (And). generate . 모든 세 가지를 커밋하고, 스키마 변경 후 다시 실행하십시오. 이미 배포된 SQL 파일은 편집하지 마십시오.
3. 번들러가 .sql을 텍스트로 임포트하도록 허용하십시오
migrations.js 라인이 포함된 SQL 파일이 있습니다. import m0000 from './0000_fearless_selene.sql'. vite는 알지 못합니다. .sql, 따라서 또 다른 의존성을 추가하는 대신에 작은 플러그인을 추가하세요.
// 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 };
}
},
},
],
});
그리고 타입 선언을 추가하여 TypeScript가 import를 받아들이도록 하세요.
// src/sql.d.ts
declare module '*.sql' {
const content: string;
export default content;
}
웹팩 (Angular 커스텀 빌더, 이전 설정) 사용 시 asset/source 파일 대신 .sql 4. 시작 시 마이그레이션을 적용하세요.
이것은 Drizzle의 마이그레이터가 사용하는 동일한 규칙을 따릅니다: 마이그레이션은 자신의 일기장 시간이 가장 최근에 기록된 것보다 새로운 경우 pending 상태입니다. 각 마이그레이션은 자신의 트랜잭션에서 실행되므로 실패 시 이전 상태가 유지됩니다. 시작 시마다 호출하는 것은 저렴합니다.
// 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;
}
}
}
javascript를
__CAPGO_KEEP_0__ live updates Capgo 라이브 업데이트쿼리
Queries
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);
관계형 쿼리
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은 SQLite의 JSON 함수를 사용하여 이러한 쿼리를 빌드하고 결과를 파싱합니다. iOS와 최근 안드로이드 버전에서 작동합니다. 만약 당신의 안드로이드 버전이旧하면, minSdkVersion 에서 확인하세요. SELECT json('{}') 조인: 별칭 중복된 열 이름
조인: 별칭이 같은 열 이름
두 개의 열이 AS 이름을 합쳐진 열에 대한 별칭을 지정하기 때문에 이 쿼리:
// 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));
이 id으로 반환되었습니다. 에러는 발생하지 않습니다. postId 조인: 별칭 중복된 열 이름 userId was undefined두 개의 열이 1로 이름이 지정됩니다. 이 열은 하나의 키로 합쳐지며, 배열은 하나의 값이 부족하고, Drizzle은 잘못된 필드에 값을 할당합니다.私の테스트에서는 사용자 ID와 2가 1으로 반환되었습니다. 에러는 발생하지 않습니다.
Alias를 명시적으로 지정하십시오:
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));
다른 이름을 가진 열을 선택하는 것은 (posts.title, users.name)은 문제가 없습니다. 하지만 두 테이블이 같은 열 이름을 공유하는 경우에는 db.select().from(a).innerJoin(b, ...) 또는 id 을 피하십시오. created_at. 관계형 쿼리는 이 문제를 해결하지 못합니다.
트랜잭션
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' });
});
});
콜백이 예외를 발생시키면 Drizzle은 rollback를 호출합니다. tx.rollback() 또한 트랜잭션을 취소합니다.
데이터베이스당 하나의 연결이 있으므로 트랜잭션을 짧게 유지하고 네트워크 호출을 트랜잭션 내에서 수행하지 마십시오. 다른 쓰기 작업은 열린 트랜잭션 내에서 실행됩니다. await 네트워크 호출은 그 안에서 실행됩니다. 다른 쓰기 작업은 동일한 연결에서 열린 트랜잭션 내에서 실행됩니다.
문제 해결
시작 시 '연결이 이미 존재합니다' 오류 Your 초기화가 두 번 실행되었습니다. 일반적으로热 재로드로 인해 발생합니다. Use the checkConnectionsConsistency 그리고 retrieveConnection 패턴이 위에 표시되어 있습니다.
cannot start a transaction within a transaction. A run() 호출이 3 번째 인수로 제공되지 않아 Drizzle 내부에서 플러그인이 자신의 트랜잭션을 열었습니다. false 런칭 시 매이그레이션을 실행하고 실패합니다. 'insert'가 누락되거나 롤백되었습니다. Check that the journal
값이 데이터베이스의 행과 일치하는지 확인하세요. table already exists. values __drizzle_migrations in when your
Build 오류 Failed to parse source for import analysis on .sql. Vite 변환 플러그인은 누락되거나 다른 플러그인이 파일을 파싱하려는 플러그인보다 먼저 실행됩니다. 배열의 첫 번째에 두세요. plugins returns 빈 배열.
.returning() 파일 쓰기가 성공했습니다 return 모드 run . 반환 모드에서 드라이버가 nowhen 'all' is not method is not run.
초를 저장합니다, mode: 'timestamp' seconds mode: 'timestamp_ms' 밀리초 단위로 저장합니다. 열을 하나씩 선택하고 raw와 섞이지 않도록 합니다. Date.now() 삽입합니다.
어떤 것을 선택해야 할 때
- Fast SQL을 사용하여 가장 빠른 데이터 경로를 원하거나, 이름으로 키가 된 행 객체: Kysely와 Capacitor.
- 데코레이터, 엔티티 및 리포지토리를 선호하거나 TypeORM 백엔드 모델을 포팅하고 싶다면: TypeORM과 Capacitor.
- REST에서 암호화가 필요합니다: Capacitor에서 SQLite 데이터베이스 암호화.
네이티브 플러그인을 추가하면 iOS 및 Android 앱을 다시 빌드해야 합니다. Mac이 없다면: Capgo 빌드 클라우드에서 두 개 모두 빌드하고 서명합니다.