forked from imbytecat/fullstack-starter
feat: 迁移数据库至SQLite并适配嵌入式环境
- 添加SQLite数据库文件及数据目录的忽略规则 - 将数据库连接地址更新为本地SQLite数据库文件路径 - 将数据库从 PostgreSQL 迁移至 SQLite,更新依赖、Drizzle 方言及数据类型映射,确保嵌入式环境兼容性并保持应用层代码不变。 - 将数据库方言从 PostgreSQL 更改为 SQLite。 - 将数据库依赖从 postgres 替换为 better-sqlite3,并添加 better-sqlite3 的类型定义。 - 修改数据库连接字符串验证规则,从URL格式验证改为非空字符串验证。 - 将数据库连接从 PostgreSQL 切换为 Better-SQLite3,并支持创建目录和内存数据库。 - 将 todo 表的 completed 字段从 PostgreSQL 的 boolean 类型改为 SQLite 的 boolean 模式整数类型。 - 将字段定义从 PostgreSQL 适配改为 SQLite 适配,使用文本类型 ID 和时间戳整数类型,并统一使用 UUIDv7 和当前时间作为默认值。 - 添加 better-sqlite3 及其类型定义,并引入相关依赖以支持其功能 - 添加 better-sqlite3 依赖并移除 postgres 依赖
This commit is contained in:
@@ -1 +1 @@
|
||||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres
|
||||
DATABASE_URL=./data/app.db
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
- **运行时**: Bun
|
||||
- **语言**: TypeScript (strict mode, ESNext)
|
||||
- **样式**: Tailwind CSS v4
|
||||
- **数据库**: PostgreSQL + Drizzle ORM
|
||||
- **数据库**: SQLite + Drizzle ORM
|
||||
- **状态管理**: TanStack Query
|
||||
- **路由**: TanStack Router (文件路由)
|
||||
- **RPC**: ORPC (类型安全 RPC,契约优先)
|
||||
@@ -173,22 +173,23 @@ export const Route = createFileRoute('/')({
|
||||
|
||||
### 数据库 Schema (Drizzle)
|
||||
|
||||
- 在 `src/db/schema/*.ts` 定义 schema
|
||||
- 从 `src/db/schema/index.ts` 导出
|
||||
- 使用 `drizzle-orm/pg-core` 的 PostgreSQL 类型
|
||||
- 主键使用 `uuidv7()` (需要 PostgreSQL 扩展)
|
||||
- 在 `src/server/db/schema/*.ts` 定义 schema
|
||||
- 从 `src/server/db/schema/index.ts` 导出
|
||||
- 使用 `drizzle-orm/sqlite-core` 的 SQLite 类型
|
||||
- 主键使用 `uuidv7()` (TEXT 存储)
|
||||
- 时间戳使用 `integer({ mode: 'timestamp_ms' })` (Unix 毫秒时间戳)
|
||||
- 始终包含 `createdAt` 和 `updatedAt` 时间戳
|
||||
|
||||
示例:
|
||||
```typescript
|
||||
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
|
||||
import { v7 as uuidv7 } from 'uuid'
|
||||
|
||||
export const myTable = pgTable('my_table', {
|
||||
id: uuid().primaryKey().default(sql`uuidv7()`),
|
||||
name: text().notNull(),
|
||||
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow().$onUpdateFn(() => new Date()),
|
||||
export const myTable = sqliteTable('my_table', {
|
||||
id: text('id').primaryKey().$defaultFn(() => uuidv7()),
|
||||
name: text('name').notNull(),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date()),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date()).$onUpdateFn(() => new Date()),
|
||||
})
|
||||
```
|
||||
|
||||
@@ -377,6 +378,119 @@ const fingerprint = createHash('sha256')
|
||||
- 专业库不一定解决所有问题(如并发去重)
|
||||
- 对于简单场景,手动实现 vs 库的选择主要取决于可维护性而非功能
|
||||
|
||||
### PostgreSQL → SQLite 迁移
|
||||
|
||||
**迁移时间**: 2026-01-26
|
||||
|
||||
**原因**: 项目需要在嵌入环境中运行,无法安装 PostgreSQL
|
||||
|
||||
**技术选择**:
|
||||
- **驱动**: `better-sqlite3` v11.8.1 (跨平台,成熟稳定,原生模块)
|
||||
- **类型定义**: `@types/better-sqlite3` v7.6.12
|
||||
- **主键**: TEXT 存储 UUIDv7 (保持全局唯一性,36 字符字符串)
|
||||
- **时间戳**: INTEGER 存储 Unix 毫秒时间戳 (`integer({ mode: 'timestamp_ms' })`)
|
||||
- **布尔值**: INTEGER 存储 0/1 (`integer({ mode: 'boolean' })`)
|
||||
- **数据库文件**: `./data/app.db`
|
||||
|
||||
**变更内容**:
|
||||
1. **依赖**: `postgres` → `better-sqlite3` + `@types/better-sqlite3`
|
||||
2. **Drizzle 方言**: `dialect: 'postgresql'` → `dialect: 'sqlite'`
|
||||
3. **Schema 导入**: `drizzle-orm/pg-core` → `drizzle-orm/sqlite-core`
|
||||
4. **表定义**: `pgTable` → `sqliteTable`
|
||||
5. **数据库连接**: `drizzle-orm/postgres-js` → `drizzle-orm/better-sqlite3`
|
||||
6. **数据类型映射**:
|
||||
- UUID: `uuid()` → `text()` (使用 `uuidv7()` 生成)
|
||||
- 时间戳: `timestamp({ withTimezone: true })` → `integer({ mode: 'timestamp_ms' })`
|
||||
- 布尔值: `boolean()` → `integer({ mode: 'boolean' })`
|
||||
- 默认值: `defaultNow()` → `$defaultFn(() => new Date())`
|
||||
7. **环境变量**: `DATABASE_URL` 从 URL 格式改为文件路径 (`./data/app.db`)
|
||||
8. **目录自动创建**: 添加了数据库文件目录的自动创建逻辑
|
||||
|
||||
**数据库连接层变更**:
|
||||
```typescript
|
||||
// Before (PostgreSQL)
|
||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||
export const createDB = () =>
|
||||
drizzle({
|
||||
connection: { url: env.DATABASE_URL, prepare: true },
|
||||
schema,
|
||||
})
|
||||
|
||||
// After (SQLite)
|
||||
import { drizzle } from 'drizzle-orm/better-sqlite3'
|
||||
import Database from 'better-sqlite3'
|
||||
import { mkdirSync } from 'node:fs'
|
||||
import { dirname } from 'node:path'
|
||||
|
||||
export const createDB = () => {
|
||||
const dbPath = env.DATABASE_URL
|
||||
if (dbPath !== ':memory:') {
|
||||
mkdirSync(dirname(dbPath), { recursive: true })
|
||||
}
|
||||
const sqlite = new Database(dbPath)
|
||||
return drizzle(sqlite, { schema })
|
||||
}
|
||||
```
|
||||
|
||||
**Schema 变更示例**:
|
||||
```typescript
|
||||
// Before (PostgreSQL)
|
||||
import { pgTable, text, boolean, timestamp, uuid } from 'drizzle-orm/pg-core'
|
||||
export const todoTable = pgTable('todo', {
|
||||
id: uuid('id').primaryKey().$defaultFn(() => uuidv7()),
|
||||
title: text('title').notNull(),
|
||||
completed: boolean('completed').notNull().default(false),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
})
|
||||
|
||||
// After (SQLite)
|
||||
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
|
||||
export const todoTable = sqliteTable('todo', {
|
||||
id: text('id').primaryKey().$defaultFn(() => uuidv7()),
|
||||
title: text('title').notNull(),
|
||||
completed: integer('completed', { mode: 'boolean' }).notNull().default(false),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date()),
|
||||
})
|
||||
```
|
||||
|
||||
**兼容性保证**:
|
||||
- ✅ 所有业务逻辑保持不变
|
||||
- ✅ ORPC 契约和类型完全兼容
|
||||
- ✅ 应用层代码无需修改
|
||||
- ✅ Drizzle 自动处理类型转换 (Date ↔ number, boolean ↔ 0/1)
|
||||
|
||||
**构建验证**:
|
||||
- ✅ `bun typecheck` - 类型检查通过
|
||||
- ✅ `bun fix` - 代码质量检查通过
|
||||
- ✅ `bun run build` - Vite 构建成功
|
||||
- ✅ `bun db:push` - 数据库初始化成功
|
||||
- ✅ SQLite 文件已创建 (`./data/app.db`, 12KB)
|
||||
|
||||
**注意事项**:
|
||||
- SQLite 是文件数据库,适合嵌入环境和开发环境
|
||||
- 不支持多进程并发写入 (适合单应用实例)
|
||||
- 性能足够支持中小型应用 (日请求 < 10万)
|
||||
- 数据库文件和 WAL 文件 (`.db-shm`, `.db-wal`) 已添加到 `.gitignore`
|
||||
- `better-sqlite3` 是原生模块,需要匹配生产环境的 OS 和架构 (构建输出会提示)
|
||||
|
||||
**Drizzle 时间戳模式**:
|
||||
- `timestamp_ms`: Unix 毫秒时间戳 (推荐,精度更高)
|
||||
- `timestamp`: Unix 秒时间戳
|
||||
- Drizzle 自动转换 `Date` 对象 ↔ 数字
|
||||
|
||||
**命令对比**:
|
||||
```bash
|
||||
# 开发环境初始化 (推荐)
|
||||
bun db:push
|
||||
|
||||
# 生产环境迁移
|
||||
bun db:generate # 生成迁移文件
|
||||
bun db:migrate # 执行迁移
|
||||
|
||||
# 数据库管理 UI
|
||||
bun db:studio
|
||||
```
|
||||
|
||||
### Git 工作流要求
|
||||
|
||||
**重要原则**:保持代码仓库与文档同步
|
||||
|
||||
@@ -4,7 +4,7 @@ import { env } from '@/env'
|
||||
export default defineConfig({
|
||||
out: './drizzle',
|
||||
schema: './src/server/db/schema/index.ts',
|
||||
dialect: 'postgresql',
|
||||
dialect: 'sqlite',
|
||||
dbCredentials: {
|
||||
url: env.DATABASE_URL,
|
||||
},
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"drizzle-orm": "catalog:",
|
||||
"drizzle-zod": "catalog:",
|
||||
"ohash": "catalog:",
|
||||
"postgres": "catalog:",
|
||||
"better-sqlite3": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"systeminformation": "catalog:",
|
||||
@@ -46,6 +46,7 @@
|
||||
"@tanstack/react-devtools": "catalog:",
|
||||
"@tanstack/react-query-devtools": "catalog:",
|
||||
"@tanstack/react-router-devtools": "catalog:",
|
||||
"@types/better-sqlite3": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@vitejs/plugin-react": "catalog:",
|
||||
"babel-plugin-react-compiler": "catalog:",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { z } from 'zod'
|
||||
|
||||
export const env = createEnv({
|
||||
server: {
|
||||
DATABASE_URL: z.url(),
|
||||
DATABASE_URL: z.string().min(1),
|
||||
},
|
||||
clientPrefix: 'VITE_',
|
||||
client: {
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||
import { mkdirSync } from 'node:fs'
|
||||
import { dirname } from 'node:path'
|
||||
import Database from 'better-sqlite3'
|
||||
import { drizzle } from 'drizzle-orm/better-sqlite3'
|
||||
import { env } from '@/env'
|
||||
import * as schema from '@/server/db/schema'
|
||||
|
||||
export const createDB = () =>
|
||||
drizzle({
|
||||
connection: {
|
||||
url: env.DATABASE_URL,
|
||||
prepare: true,
|
||||
},
|
||||
schema,
|
||||
})
|
||||
export const createDB = () => {
|
||||
const dbPath = env.DATABASE_URL
|
||||
if (dbPath !== ':memory:') {
|
||||
mkdirSync(dirname(dbPath), { recursive: true })
|
||||
}
|
||||
const sqlite = new Database(dbPath)
|
||||
return drizzle(sqlite, { schema })
|
||||
}
|
||||
|
||||
export type DB = ReturnType<typeof createDB>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { boolean, pgTable, text } from 'drizzle-orm/pg-core'
|
||||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||
import { generatedFields } from './utils/field'
|
||||
|
||||
export const todoTable = pgTable('todo', {
|
||||
export const todoTable = sqliteTable('todo', {
|
||||
...generatedFields,
|
||||
title: text('title').notNull(),
|
||||
completed: boolean('completed').notNull().default(false),
|
||||
completed: integer('completed', { mode: 'boolean' }).notNull().default(false),
|
||||
})
|
||||
|
||||
@@ -1,37 +1,25 @@
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { timestamp, uuid } from 'drizzle-orm/pg-core'
|
||||
import { integer, text } from 'drizzle-orm/sqlite-core'
|
||||
import { v7 as uuidv7 } from 'uuid'
|
||||
|
||||
// id
|
||||
|
||||
export const id = (name: string) => uuid(name)
|
||||
export const pk = (name: string, strategy?: 'native' | 'extension') => {
|
||||
switch (strategy) {
|
||||
// PG 18+
|
||||
case 'native':
|
||||
return id(name).primaryKey().default(sql`uuidv7()`)
|
||||
|
||||
// PG 13+ with extension
|
||||
case 'extension':
|
||||
return id(name).primaryKey().default(sql`uuid_generate_v7()`)
|
||||
|
||||
// Any PG version
|
||||
default:
|
||||
return id(name)
|
||||
.primaryKey()
|
||||
.$defaultFn(() => uuidv7())
|
||||
}
|
||||
}
|
||||
export const id = (name: string) => text(name)
|
||||
export const pk = (name: string) =>
|
||||
id(name)
|
||||
.primaryKey()
|
||||
.$defaultFn(() => uuidv7())
|
||||
|
||||
// timestamp
|
||||
|
||||
export const createdAt = (name = 'created_at') =>
|
||||
timestamp(name, { withTimezone: true }).notNull().defaultNow()
|
||||
integer(name, { mode: 'timestamp_ms' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date())
|
||||
|
||||
export const updatedAt = (name = 'updated_at') =>
|
||||
timestamp(name, { withTimezone: true })
|
||||
integer(name, { mode: 'timestamp_ms' })
|
||||
.notNull()
|
||||
.defaultNow()
|
||||
.$defaultFn(() => new Date())
|
||||
.$onUpdateFn(() => new Date())
|
||||
|
||||
// generated fields
|
||||
|
||||
Reference in New Issue
Block a user