在 Electron 中使用 SQLite 的最好方式

Electron中使用SQLite的最好方式

Apr 22, 2024 ·
6分钟阅读

上周刚刚发布了一个用Electron的应用 EpubKit. EpubKit是一个把网页制作成电子书的工具。在EpubKit里,我需要一个数据库来存储内容,最好的选择是SQLite.

但是,由于Electronrenderermain区分开的机制,所以在Electron中使用SQLite会非常麻烦 —— SQLite的执行要在Main process,但调用要在Renderer process.Electron里,RendererMain process之间的通信是通过IPC (Inter-Process Communication)实现的。也就是说,我可能需要把每一个有关数据库操作的业务逻辑单独写成一个IPC通信的事件,然后在Renderer里调用这些事件。

我想要做到的是,我在Renderer process中直接调用ORM,但实际的执行是在Main process中。这样一来我就不需要单独地写很多个IPC事件了。

例如:

万幸的是 drizzle 居然有一个 HTTP Proxy 的机制。这个机制能让你所有的ORM操作都流到一个地方,在这个地方你能拿到最终生成的sql语句,然后你可以自己决定怎么执行这个sql语句。

也就是说,我可以在这个proxy里,把sql语句通过IPC发送到Main process,然后在Main process里执行这个sql语句。

接下来我会简单描述一下我在EpubKit里是怎么做的。

编写Schema

在你的项目里找一个地方,把drizzle schema写下来:

import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const posts = sqliteTable('posts', {
id: int("id").primaryKey().default(0),
title: text("title").notNull().default(""),
})

Renderer里创建一个drizzle database实例

根据 文档,在创建drizzle db实例的时候,可以传入一个函数,这就是proxy的本体。我们要做的是在这个proxy里,拿到ORM最终生成的sql语句、执行方法、变量,然后通过IPC发送到Main process.

export const database = drizzle(async (...args) => {
try {
// 通过 IPC 把 SQL 发送到 Main process
const result = await window.api.execute(...args)
return {rows: result}
} catch (e: any) {
console.error('Error from sqlite proxy server: ', e.response.data)
return { rows: [] }
}
}, {
schema: schema
})

这里有一个window.api.execute(),是怎么来的呢?其实是在 preload 进程里面定义然后暴露的,它的作用就是通过IPC发送sql语句到Main process:

preload.ts
const api = {
execute: (...args) => ipcRenderer.invoke('db:execute', ...args),
}

也就是说,实际上我们以上做的事情就是,通过proxy,SQL语句通过Main process里的db:executehandler最终执行。

Main process

Main process,我们创建一个IPC handler:

main.ts
ipcMain.handle('db:execute', execute)

这里的 execute 就是Main process里最终执行SQL语句的函数。

import { drizzle } from 'drizzle-orm/better-sqlite3'
import Database from 'better-sqlite3'
import { migrate } from 'drizzle-orm/better-sqlite3/migrator'
import * as schema from '../renderer/src/db/schema'
import fs from 'fs'
import { app } from 'electron'
import path from 'path'
// 初始化 sqlite
const dbPath = '../databse.db'
fs.mkdirSync(path.dirname(dbPath), { recursive: true })
const sqlite = new Database(
dbPath
)
// 创建 drizzle 实例
export const db = drizzle(sqlite, { schema })
// 这里是 execute 方法
export const execute = async (e, sqlstr, params, method) => {
// 得到执行需要的参数后,用 better-sqlite3 执行
const result = sqlite.prepare(sqlstr)
const ret = result[method](...params)
return toDrizzleResult(ret)
}
function toDrizzleResult(row: Record<string, any>)
function toDrizzleResult(rows: Record<string, any> | Array<Record<string, any>>) {
if (!rows) {
return []
}
if (Array.isArray(rows)) {
return rows.map((row) => {
return Object.keys(row).map((key) => row[key])
})
} else {
return Object.keys(rows).map((key) => rows[key])
}
}

在上面的代码中,我额外实现了一个 toDrizzleResult 的方法,是为了把better-sqlite3的返回值按照drizzle需要的结构返回。

到这里,你就已经可以在Renderer process里直接用drizzle了:

function App(): JSX.Element {
const [postList, setPosts] = useState([] as any[])
useEffect(() => {
database.query.posts.findMany().then(result => {
setPosts(result)
})
}, [])
return (
<div>
<div>
<form onSubmit={async e => {
e.preventDefault()
const formData = new FormData(e.target as HTMLFormElement)
const title = formData.get('title') as string
if (title) {
await database.insert(posts).values({
id: Math.floor(Math.random() * 1000),
title
})
// refetch
const result = await database.query.posts.findMany()
setPosts(result)
}
}}>
<input name="title" type="text" placeholder="title" />
<button>add</button>
</form>
</div>
{postList.map(post => {
return (
<div key={post.id}>
{post.title}
</div>
)
})}
</div>
)
}
export default App

但这时候执行,会报错。原因是我们还没有初始化数据库。我们需要在Main process里初始化数据库。

首先需要用drizzle-kit生成migration文件。在 drizzle.config.ts 中指定了migration文件的地址:

drizzle.config.ts
import type { Config } from 'drizzle-kit'
export default {
schema: './src/db/schema.ts',
out: './drizzle',
driver: 'better-sqlite'
} satisfies Config

然后写一个 runMigrations 方法,用来初始化数据库:

export const runMigrate = async () => {
migrate(db, {
// 在 drizzle.config.ts 里指定的路径
migrationsFolder: path.join(__dirname, '../../drizzle')
})
}

这个方法需要在Main process启动时执行的:

async function createWindow() {
// ...
await runMigrate()
createWindow()
//...
}

实例源码

你可以在 这里 找到这个示例的完整源码。

特别感谢 EGOIST 提供灵感。


sponsor
sponsor
通过支付宝[email protected]或赞赏码赞助此文