跳到主要内容
Void

数据库

用 TypeScript 定义表,然后用 void/db 的 db 查询它们。Void 内置 Drizzle ORM,并为你配置好连接。

预计阅读 8 分钟

├── schema.ts            表定义(唯一真相来源)
├── schema/              拆分 schema 文件(可选)
│   ├── users.ts
│   └── posts.ts
├── migrations/          生成的 SQL 迁移文件
│   ├── 20260410161500_create_users.sql
│   └── 20260410161501_add_posts.sql
├── seed.ts              程序化种子脚本(可选)
└── seed.sql             原始 SQL 种子文件(可选替代)

选择方言

在 void init 期间选择数据库,或在 void.config.ts 中设置 database:

方言 配置 托管方 连接方式
D1(SQLite) 默认 Void 托管 自动的 D1 绑定
PostgreSQL "database": "pg" 自带数据库 Hyperdrive
MySQL "database": "mysql" 自带数据库 Hyperdrive

定义 schema

在 db/schema.ts 中使用来自 void/schema-d1、void/schema-pg 或 void/schema-mysql 的列辅助函数定义表。

import { sql } from 'void/db';
import { sqliteTable, text, integer } from 'void/schema-d1';

export const users = sqliteTable('users', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  role: text('role').notNull().default('user'),
  createdAt: text('created_at')
    .notNull()
    .default(sql`(datetime('now'))`),
});
import { pgTable, serial, text, timestamp } from 'void/schema-pg';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  role: text('role').notNull().default('user'),
  createdAt: timestamp('created_at').notNull().defaultNow(),
});
import { int, mysqlTable, text, timestamp } from 'void/schema-mysql';

export const users = mysqlTable('users', {
  id: int('id').autoincrement().primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  createdAt: timestamp('created_at').notNull().defaultNow(),
});

schema 辅助函数已随 void 提供。你可以直接手写表,也可以用 void gen model 生成一个起点。

你也可以把 schema 拆分到 db/schema/ 下的多个文件,并从 barrel 文件重新导出:

export const users = /* ... */;
export * from './schema/users';
export * from './schema/posts';

查询

Void 会从 void/db 重新导出所有常用的 Drizzle 查询运算符,因此你不需要单独安装 drizzle-orm。从 void/db 导入 db,从 @schema 导入你的表。

import { users } from '@schema';
import { db } from 'void/db';

const allUsers = await db.select().from(users);

筛选

import { users } from '@schema';
import { db, eq, and, or } from 'void/db';

// 单个条件
const user = await db.select().from(users).where(eq(users.id, 1));

// 多个条件
const admins = await db
  .select()
  .from(users)
  .where(and(eq(users.role, 'admin'), eq(users.name, 'Alice')));

// 或条件
const editors = await db
  .select()
  .from(users)
  .where(or(eq(users.role, 'admin'), eq(users.role, 'editor')));

插入

// 单行
await db.insert(users).values({
  name: 'Alice',
  email: 'alice@example.com',
});

// 插入并返回新建的行
const [created] = await db
  .insert(users)
  .values({ name: 'Bob', email: 'bob@example.com' })
  .returning();

// 多行
await db.insert(users).values([
  { name: 'Alice', email: 'alice@example.com' },
  { name: 'Bob', email: 'bob@example.com' },
]);

更新与删除

await db.update(users).set({ role: 'admin' }).where(eq(users.id, 1));

const [updated] = await db.update(users).set({ role: 'admin' }).where(eq(users.id, 1)).returning();

await db.delete(users).where(eq(users.id, 1));

列选择、排序与分页

import { users } from '@schema';
import { db, desc } from 'void/db';

const names = await db.select({ name: users.name, email: users.email }).from(users);
// names: { name: string; email: string }[]

const page = await db.select().from(users).orderBy(desc(users.createdAt)).limit(10).offset(20);

关联

import { users, posts } from '@schema';
import { db, eq } from 'void/db';

const results = await db
  .select({
    id: posts.id,
    title: posts.title,
    author: users.name,
  })
  .from(posts)
  .innerJoin(users, eq(posts.userId, users.id));

如果 schema 中定义了 relations,还可以使用 Drizzle 的关系查询 API:

const usersWithPosts = await db.query.users.findMany({
  with: { posts: true },
});

种子数据

用 void db seed 重置本地数据库、重新应用迁移,然后运行一个种子文件。

用 db/seed.ts 做程序化种子,用 db/seed.sql 写 SQL。JavaScript 以及 .mts / .mjs 文件同样可用。如果存在多个种子文件,用 --file <path> 选择其中一个。

import { defineSeed } from 'void/seed';

export default defineSeed<typeof import('./schema')>(async ({ db, schema }) => {
  const rows = Array.from({ length: 100 }, (_, i) => ({
    text: `Seed message ${i + 1}`,
  }));

  await db.insert(schema.messages).values(rows);
});

种子上下文包含:

  • dialect:"sqlite"、"postgresql" 或 "mysql"
  • db:指向本地数据库的 Drizzle 实例
  • schema:来自 db/schema.ts 或 db/schema/ 的导出
INSERT INTO messages (text) VALUES ('Hello from SQL');

从 schema 派生校验器

用 void/drizzle-zod、void/drizzle-valibot 或 void/drizzle-arktype 从表中派生请求校验器。在表定义之后添加它:

import { createInsertSchema } from 'void/drizzle-zod';

export const insertUserSchema = createInsertSchema(users, {
  name: (schema) => schema.min(1),
  email: (schema) => schema.email(),
});
import { pipe, minLength, email } from 'valibot';
import { createInsertSchema } from 'void/drizzle-valibot';

export const insertUserSchema = createInsertSchema(users, {
  name: (schema) => pipe(schema, minLength(1)),
  email: (schema) => pipe(schema, email()),
});
import { type } from 'arktype';
import { createInsertSchema } from 'void/drizzle-arktype';

export const insertUserSchema = createInsertSchema(users, {
  name: type('string > 0'),
  email: type('string.email'),
});

createInsertSchema 会生成与 $inferInsert 匹配的校验器:有默认值或自增的列变为可选,NOT NULL 列保持必填。可选的第二个参数让你逐列细化,既可以传入一个接收生成 schema 的回调,也可以直接传入一个类型来覆盖某个字段。

在路由处理器中用 withValidator() 使用派生的 schema。三个可用函数:

函数 用途
createInsertSchema 校验插入数据,排除自动生成的列并应用默认值
createSelectSchema 匹配查询结果行的结构
createUpdateSchema 与 insert 类似,但所有字段都是可选的(部分更新)

CLI 命令

命令 用途
void db push 在本地应用 schema 变更,不生成迁移文件
void db generate 生成 SQL 迁移
void db migrate 在本地应用待处理的迁移
void db status 检查 schema 漂移与待处理迁移
void db seed 重置本地数据库并运行种子文件

脚手架

void gen model 会生成适配当前方言的 sqliteTable、pgTable 或 mysqlTable 定义:

void gen model posts title:string body:text published:boolean

它会创建:

  • db/schema/posts.ts:包含 id、createdAt、updatedAt 与你指定列的 Drizzle 表定义
  • 更新 db/schema.ts,加入 export * from "./schema/posts"
  • routes/api/posts/index.ts:列表 GET 与带校验的插入 POST
  • routes/api/posts/[id].ts:按 id 查询的 GET,带 404 处理

手写迁移

部署时,Void 会把 Drizzle schema 与 db/migrations/meta/ 中的快照做比较。如果你有 SQL 迁移却没有这份元数据,运行 void db generate 会为同样的表再创建一个迁移。

要保留已有的迁移历史:让 Drizzle schema 与你的 SQL 迁移所创建的表保持一致,在项目的一份临时副本中删除 db/migrations/ 后运行 void db generate,再把生成的 meta/ 目录复制回你的迁移目录。在 _journal.json 中,为每个已有的 SQL 文件按顺序替换条目,idx 从 0 连续递增,tag 为去掉 .sql 的文件名,when 使用递增的时间戳,version 使用生成条目的版本。快照文件重命名为 <prefix>_snapshot.json,其中 <prefix> 是最后一个迁移 tag 在第一个 _ 之前的部分。例如 001_create_users.sql 对应 001_snapshot.json。

D1 的特别说明

Void 在开通数据库时会启用 D1 读副本。通过 void/db 的 db 发出的查询,会为每个 Void 请求使用一个 D1 session,因此读取可以在靠近请求的位置执行,且同一请求中后续的查询能看到至少相同的数据。

使用 cloudflare:workers 中的 env.DB 可以配合其他查询库或原生 D1 API:

import { env } from 'cloudflare:workers';
import { Kysely } from 'kysely';
import { D1Dialect } from 'kysely-d1';

const db = new Kysely<Database>({ dialect: new D1Dialect({ database: env.DB }) });
import { env } from 'cloudflare:workers';

const { results } = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).all();

无论使用哪种查询层,Void 仍然处理绑定推断与部署。

目录