Skip to content

集成 drizzle

以 SQLite 为例

安装依赖

bash
pnpm add drizzle-orm better-sqlite3
pnpm add -D drizzle-kit @types/better-sqlite3
详细说明
  • drizzle-orm:Drizzle ORM,用于定义 schema 和类型安全查询。
  • drizzle-kit:Drizzle 迁移工具,根据 schema 生成迁移 SQL。
  • better-sqlite3:SQLite 驱动,原生绑定,同步 API。
  • @types/better-sqlite3:提供 better-sqlite3 的 TypeScript 类型定义。

配置原生模块重建:

json
{
  "scripts": {
    "postinstall": "electron-builder install-app-deps"
  },
  "pnpm": {
    "onlyBuiltDependencies": ["better-sqlite3", "electron", "esbuild"]
  }
}

electron-builder.yml

yaml
npmRebuild: true
asarUnpack:
  - '**/*.node'

常见问题:

现象处理
Module did not self-register运行 pnpm postinstall
开发正常、打包后崩溃检查 npmRebuild 和 ABI
pnpm 跳过 build 脚本配置 onlyBuiltDependencies
Windows missing any VC++ toolset安装 MSVC 构建工具

目录结构

bash
src/
├── main/
   ├── db/
   ├── client.ts       # better-sqlite3 连接 + Drizzle 实例
   └── schema.ts       # Drizzle 表定义
   ├── ipc/
   └── settings.ts     # ipcMain.handle
   ├── repositories/       # 数据访问层
   ├── services/           # 业务服务层
   └── index.ts
├── preload/
   └── index.ts            # contextBridge -> window.api
└── renderer/
    └── src/

数据流:

bash
renderer -> preload -> ipcRenderer.invoke -> ipcMain.handle -> service -> repository -> SQLite

数据库连接

SQLite 文件建议放在 app.getPath('userData') 下。应用升级后该目录保持不变,用户数据不会因为安装目录变化而丢失。

ts
import Database from 'better-sqlite3'
import { app } from 'electron'
import { drizzle } from 'drizzle-orm/better-sqlite3'
import { mkdirSync } from 'node:fs'
import { join } from 'node:path'
import * as schema from './schema'

let db: ReturnType<typeof drizzle<typeof schema>> | null = null

export function getDatabase() {
  if (db) return db

  const dbDir = join(app.getPath('userData'), 'database')
  mkdirSync(dbDir, { recursive: true })

  const sqlite = new Database(join(dbDir, 'app.sqlite'))
  sqlite.pragma('journal_mode = WAL')
  sqlite.pragma('foreign_keys = ON')

  db = drizzle(sqlite, { schema })
  return db
}

要点:

  • 主进程单例复用连接,不要每次 IPC 都 new Database()
  • 启用 WAL,提升读写并发体验。
  • 启用 foreign_keys,避免 SQLite 默认外键不生效的问题。
  • 数据库路径不要放在安装目录,安装目录可能没有写权限。

Schema

ts
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'

export const settingsTable = sqliteTable('settings', {
  key: text('key').primaryKey(),
  value: text('value').notNull(),
  updatedAt: integer('updated_at').notNull(),
})

export type Setting = typeof settingsTable.$inferSelect
export type NewSetting = typeof settingsTable.$inferInsert

$inferSelect$inferInsert 从 schema 推断查询结果和插入数据类型。

查询

Drizzle 配合 better-sqlite3 使用同步方法:

  • .get():查询单行。
  • .all():查询多行。
  • .run():执行写入。
ts
import { eq } from 'drizzle-orm'
import { getDatabase } from './client'
import { settingsTable } from './schema'

const db = getDatabase()

const row = db.select().from(settingsTable).where(eq(settingsTable.key, 'theme')).get()

const rows = db.select().from(settingsTable).all()

db.insert(settingsTable).values({ key: 'theme', value: 'dark', updatedAt: Date.now() }).run()

db.update(settingsTable).set({ value: 'light', updatedAt: Date.now() }).where(eq(settingsTable.key, 'theme')).run()

db.delete(settingsTable).where(eq(settingsTable.key, 'temp')).run()

UPSERT:

ts
db.insert(settingsTable)
  .values({ key: 'theme', value: 'dark', updatedAt: Date.now() })
  .onConflictDoUpdate({
    target: settingsTable.key,
    set: { value: 'dark', updatedAt: Date.now() },
  })
  .run()

事务:

ts
db.transaction((tx) => {
  for (const item of items) {
    tx.insert(settingsTable).values(item).run()
  }
})

迁移

drizzle.config.ts

ts
import { defineConfig } from 'drizzle-kit'

export default defineConfig({
  schema: './src/main/db/schema.ts',
  out: './drizzle',
  dialect: 'sqlite',
  dbCredentials: {
    url: './dev.sqlite',
  },
})

生成迁移:

bash
pnpm drizzle-kit generate

阶段建议:

阶段做法
早期CREATE TABLE IF NOT EXISTS bootstrap
稳定后Drizzle Kit 生成迁移,启动时执行

长期应以 schema 和迁移为准,避免手写 DDL 与 Drizzle schema 不一致。

IPC 暴露

主进程注册有限 API:

ts
import { ipcMain } from 'electron'
import { eq } from 'drizzle-orm'
import { getDatabase } from '../db/client'
import { settingsTable } from '../db/schema'

ipcMain.handle('settings:get', (_event, key: unknown) => {
  if (typeof key !== 'string' || key.length === 0) {
    throw new Error('Invalid key')
  }

  const db = getDatabase()
  return db.select().from(settingsTable).where(eq(settingsTable.key, key)).get()
})

ipcMain.handle('settings:set', (_event, key: unknown, value: unknown) => {
  if (typeof key !== 'string' || key.length === 0) {
    throw new Error('Invalid key')
  }

  if (typeof value !== 'string') {
    throw new Error('Invalid value')
  }

  const db = getDatabase()

  db.insert(settingsTable)
    .values({ key, value, updatedAt: Date.now() })
    .onConflictDoUpdate({
      target: settingsTable.key,
      set: { value, updatedAt: Date.now() },
    })
    .run()
})

Preload 桥接

ts
import { contextBridge, ipcRenderer } from 'electron'

contextBridge.exposeInMainWorld('api', {
  settings: {
    get: (key: string) => ipcRenderer.invoke('settings:get', key),
    set: (key: string, value: string) => ipcRenderer.invoke('settings:set', key, value),
  },
})

BrowserWindow 中保持安全配置:

ts
new BrowserWindow({
  webPreferences: {
    preload: preloadPath,
    contextIsolation: true,
    nodeIntegration: false,
    sandbox: true,
  },
})

类型声明

给渲染进程补充全局类型:

ts
export {}

declare global {
  interface Window {
    api: {
      settings: {
        get: (key: string) => Promise<unknown>
        set: (key: string, value: string) => Promise<void>
      }
    }
  }
}

渲染进程调用:

ts
const theme = await window.api.settings.get('theme')
await window.api.settings.set('theme', 'dark')

Checklist

  • [ ] 安装 better-sqlite3drizzle-ormdrizzle-kit
  • [ ] 配置 postinstallonlyBuiltDependenciesnpmRebuild
  • [ ] SQLite 文件放在 app.getPath('userData')
  • [ ] 主进程单例维护数据库连接
  • [ ] schema.ts 定义表结构
  • [ ] 主进程通过 ipcMain.handle 暴露有限 API
  • [ ] preload 通过 contextBridge 暴露业务方法
  • [ ] 渲染进程只调用 window.api
  • [ ] 打包后验证数据库读写

常见问题

Windows 原生模块编译

better-sqlite3 安装时会尝试下载预编译包。下载失败时会回退到本地编译,Windows 需要 MSVC 工具链。

处理方式:

  1. 打开 Visual Studio Installer。
  2. 安装 Build Tools 2022。
  3. 勾选“使用 C++ 的桌面开发”。
  4. 重新执行 pnpm installpnpm postinstall

基于 MIT 许可发布