Hono + Drizzle ORM + SQLite:本地工具和单机服务的轻量数据库方案
wxk1991 Lv5

Hono + Drizzle ORM + SQLite:本地工具和单机服务的轻量数据库方案

SQLite 经常被低估。

很多人一听到数据库,就直接想 MySQL 或 PostgreSQL。

但如果你的 Hono 项目是:

1
2
3
4
5
6
7
本地工具
内部管理脚本
个人项目
小型后台
单机部署
低并发 API
桌面应用后端

SQLite 可能是最舒服的选择。

它不需要单独启动数据库服务,不需要连接池,不需要运维一台数据库服务器。一个文件就能存数据。

Hono + Drizzle + SQLite 的组合,适合把项目做得非常轻。

技术栈快照时间:2026-06-23。本文以 Node.js 环境和 better-sqlite3 为例,不讨论 Cloudflare D1。D1 虽然也是 SQLite 语义,但部署模型不同,应该单独看。


一、什么时候适合 SQLite

适合:

1
2
3
4
5
数据量不大
写入并发不高
部署在单台机器
希望备份简单
不想维护 MySQL/PostgreSQL

比如:

  • 个人记账工具
  • 本地爬虫任务记录
  • 小型后台配置
  • 内部工具 API
  • 离线数据处理结果
  • 轻量 CMS

不适合:

1
2
3
4
5
多实例同时写
高并发写入
复杂权限和多租户
大型报表分析
需要数据库服务级 HA

SQLite 很强,但不要把它当成所有生产系统的默认答案。


二、安装依赖

1
2
pnpm add hono drizzle-orm better-sqlite3
pnpm add -D drizzle-kit typescript tsx @types/better-sqlite3 @hono/node-server

目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
hono-sqlite/
├── data/
│ └── app.db
├── drizzle/
├── src/
│ ├── db/
│ │ ├── schema.ts
│ │ └── index.ts
│ ├── repositories/
│ ├── routes/
│ └── index.ts
├── drizzle.config.ts
└── package.json

data/app.db 可以不提交到 Git。

drizzle/ 里的迁移文件应该提交。


三、定义 schema

src/db/schema.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
import { sql } from 'drizzle-orm'

export const notes = sqliteTable('notes', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
content: text('content').notNull().default(''),
archived: integer('archived', { mode: 'boolean' }).notNull().default(false),
createdAt: text('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
updatedAt: text('updated_at').notNull().default(sql`CURRENT_TIMESTAMP`),
})

export type Note = typeof notes.$inferSelect
export type NewNote = typeof notes.$inferInsert

这里仍然使用 sqlite-core

SQLite 的时间字段常见做法是存 ISO 字符串或 Unix 时间戳。简单项目用 TEXT 存 ISO 时间很直观;更强调计算和排序时,也可以用 integer 存毫秒时间戳。


四、配置 Drizzle Kit

drizzle.config.ts

1
2
3
4
5
6
7
8
9
10
import type { Config } from 'drizzle-kit'

export default {
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'sqlite',
dbCredentials: {
url: './data/app.db',
},
} satisfies Config

生成迁移:

1
pnpm exec drizzle-kit generate

应用迁移:

1
pnpm exec drizzle-kit migrate

如果 data/ 目录不存在,先创建:

1
mkdir -p data

五、创建数据库连接

src/db/index.ts

1
2
3
4
5
6
7
8
9
10
import Database from 'better-sqlite3'
import { drizzle } from 'drizzle-orm/better-sqlite3'
import * as schema from './schema'

const sqlite = new Database('./data/app.db')

sqlite.pragma('journal_mode = WAL')
sqlite.pragma('foreign_keys = ON')

export const db = drizzle(sqlite, { schema })

两个 pragma 很重要。

foreign_keys = ON 用来启用外键约束。

journal_mode = WAL 能改善读写并发体验,是 SQLite 单机服务里很常见的配置。

注意:better-sqlite3 是同步 API。

这不一定是坏事。SQLite 本来就是本地文件数据库,同步调用在很多小型服务里反而简单稳定。但如果你有大量慢查询或高并发写入,就应该重新评估架构。


六、Repository 示例

src/repositories/note-repository.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { desc, eq } from 'drizzle-orm'
import { db } from '../db'
import { notes } from '../db/schema'

export const noteRepository = {
list() {
return db
.select()
.from(notes)
.where(eq(notes.archived, false))
.orderBy(desc(notes.id))
.limit(100)
},

findById(id: number) {
return db
.select()
.from(notes)
.where(eq(notes.id, id))
.limit(1)
.get()
},

create(input: { title: string; content?: string }) {
return db
.insert(notes)
.values({
title: input.title,
content: input.content ?? '',
})
.returning()
.get()
},

archive(id: number) {
return db
.update(notes)
.set({
archived: true,
})
.where(eq(notes.id, id))
.returning()
.get()
},
}

这里没有 await

因为 better-sqlite3 是同步驱动,Drizzle 对它的调用也是同步风格。

不要为了统一 async 形式就乱包 Promise。保持真实语义更清楚。


七、Hono 路由

src/routes/notes.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
import { noteRepository } from '../repositories/note-repository'

const route = new Hono()

const createNoteSchema = z.object({
title: z.string().min(1).max(100),
content: z.string().max(5000).optional(),
})

route.get('/', (c) => {
const items = noteRepository.list()
return c.json({ data: { items } })
})

route.get('/:id', (c) => {
const id = Number(c.req.param('id'))
const note = noteRepository.findById(id)

if (!note) {
return c.json({
error: {
code: 'NOTE_NOT_FOUND',
message: '笔记不存在',
},
}, 404)
}

return c.json({ data: note })
})

route.post(
'/',
zValidator('json', createNoteSchema),
(c) => {
const input = c.req.valid('json')
const note = noteRepository.create(input)
return c.json({ data: note }, 201)
}
)

route.patch('/:id/archive', (c) => {
const id = Number(c.req.param('id'))
const note = noteRepository.archive(id)
return c.json({ data: note })
})

export default route

主入口 src/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import notesRoute from './routes/notes'

const app = new Hono()

app.route('/api/notes', notesRoute)

serve({
fetch: app.fetch,
port: 3000,
})

八、SQLite 文件怎么管理

SQLite 最大的优势是一个文件。

但这个文件也要认真管理。

建议:

1
2
3
4
5
data/app.db 不提交 Git
定期备份 data/app.db
迁移文件提交 Git
生产部署前执行 migrate
不要多个应用实例同时写同一个文件

如果你用 Docker:

1
2
3
4
5
services:
api:
image: my-hono-api
volumes:
- ./data:/app/data

一定要把数据库文件挂载到持久化目录。

否则容器重建后数据可能没了。


九、常见坑

第一,忘记开启外键。

SQLite 默认外键行为和你想象的不一定一致,建议启动时明确:

1
sqlite.pragma('foreign_keys = ON')

第二,用 SQLite 做高并发写入。

SQLite 单写多读很强,但它不是多写高并发数据库。大量并发写入应该考虑 PostgreSQL 或 MySQL。

第三,不备份。

一个文件很方便,但也意味着你要保护这个文件。

第四,把 SQLite 当临时缓存。

如果数据重要,就按正式数据库对待:迁移、备份、恢复演练都要有。


十、最后的建议

Hono + Drizzle + SQLite 很适合把小项目做得简单、可靠、容易部署。

我的建议是:

1
2
3
4
5
本地工具和小型单机服务优先考虑 SQLite
schema 和 migration 用 Drizzle 管
启动时开启 WAL 和 foreign_keys
数据库文件做持久化和备份
并发写入明显变高时迁移到 PostgreSQL / MySQL

不要为了“像生产系统”一上来就 MySQL/PostgreSQL。

也不要因为 SQLite 简单,就不做迁移和备份。

轻量不等于随便。


参考资料