主题
集成 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-sqlite3、drizzle-orm、drizzle-kit - [ ] 配置
postinstall、onlyBuiltDependencies、npmRebuild - [ ] SQLite 文件放在
app.getPath('userData') - [ ] 主进程单例维护数据库连接
- [ ]
schema.ts定义表结构 - [ ] 主进程通过
ipcMain.handle暴露有限 API - [ ] preload 通过
contextBridge暴露业务方法 - [ ] 渲染进程只调用
window.api - [ ] 打包后验证数据库读写
常见问题
Windows 原生模块编译
better-sqlite3 安装时会尝试下载预编译包。下载失败时会回退到本地编译,Windows 需要 MSVC 工具链。
处理方式:
- 打开 Visual Studio Installer。
- 安装 Build Tools 2022。
- 勾选“使用 C++ 的桌面开发”。
- 重新执行
pnpm install或pnpm postinstall。
