Hono + Drizzle ORM + SQLite:本地工具和单机服务的轻量数据库方案
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/ 目录不存在,先创建:
五、创建数据库连接
写 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 简单,就不做迁移和备份。
轻量不等于随便。
参考资料