Contents

Drizzle ORM 实战:TypeScript 全栈类型安全的数据库访问层

为什么选择 Drizzle ORM?

在 TypeScript 全栈开发中,数据库访问层的选型一直是个痛点。Prisma 虽然流行,但运行时开销大、客户端体积臃肿;TypeORM 配置繁琐且类型推断不够智能;Kysely 虽然轻量,但缺乏 Schema 定义能力。

Drizzle ORM 填补了这个空白——它是一个 TypeScript-first 的 ORM,核心理念是:

  • 零运行时开销:生成的 SQL 就是你手写的 SQL,没有黑魔法
  • 端到端类型安全:从 Schema 定义到查询结果,全链路类型推断
  • SQL-like 语法:如果你熟悉 SQL,Drizzle 的 API 会让你感到亲切
  • 轻量高效:核心包不到 50KB,适合 Serverless 和边缘计算场景

本文将从零搭建一个完整的 Drizzle ORM 项目,涵盖 Schema 定义、数据库迁移、CRUD 操作和 Next.js 集成。

项目初始化

首先创建项目并安装依赖:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 创建项目
mkdir drizzle-demo && cd drizzle-demo
npm init -y

# 安装核心依赖
npm install drizzle-orm better-sqlite3
npm install -D drizzle-kit typescript @types/better-sqlite3 tsx

# 初始化 TypeScript
npx tsc --init

配置 tsconfig.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"]
}

定义 Schema

Drizzle 的 Schema 定义非常直观,直接映射数据库表结构。创建 src/schema.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
import { sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core";
import { sql } from "drizzle-orm";

// 用户表
export const users = sqliteTable("users", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
  avatar: text("avatar"),
  createdAt: integer("created_at", { mode: "timestamp" })
    .notNull()
    .default(sql`CURRENT_TIMESTAMP`),
});

// 文章表
export const posts = sqliteTable("posts", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  title: text("title").notNull(),
  content: text("content").notNull(),
  slug: text("slug").notNull().unique(),
  published: integer("published", { mode: "boolean" }).default(false),
  authorId: integer("author_id")
    .notNull()
    .references(() => users.id, { onDelete: "cascade" }),
  createdAt: integer("created_at", { mode: "timestamp" })
    .notNull()
    .default(sql`CURRENT_TIMESTAMP`),
  updatedAt: integer("updated_at", { mode: "timestamp" })
    .notNull()
    .default(sql`CURRENT_TIMESTAMP`),
});

// 标签表(多对多关系)
export const tags = sqliteTable("tags", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  name: text("name").notNull().unique(),
});

export const postTags = sqliteTable("post_tags", {
  postId: integer("post_id")
    .notNull()
    .references(() => posts.id, { onDelete: "cascade" }),
  tagId: integer("tag_id")
    .notNull()
    .references(() => tags.id, { onDelete: "cascade" }),
});

可以看到,Schema 定义与 SQL 建表语句几乎一一对应,但完全由 TypeScript 类型驱动。

配置 Drizzle Kit

Drizzle Kit 是官方的迁移和开发工具。创建 drizzle.config.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "./src/schema.ts",
  out: "./drizzle",
  dialect: "sqlite",
  dbCredentials: {
    url: "./local.db",
  },
});

package.json 中添加脚本:

1
2
3
4
5
6
7
8
9
{
  "scripts": {
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "db:push": "drizzle-kit push",
    "db:studio": "drizzle-kit studio",
    "dev": "tsx watch src/index.ts"
  }
}

数据库连接与初始化

创建 src/db.ts

1
2
3
4
5
6
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
import * as schema from "./schema";

const sqlite = new Database("./local.db");
export const db = drizzle(sqlite, { schema });

运行迁移创建表结构:

1
2
3
4
5
# 生成迁移文件
npm run db:generate

# 直接推送到数据库(开发环境推荐)
npm run db:push

CRUD 操作实战

创建 src/index.ts,演示完整的 CRUD 操作:

  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
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
import { db } from "./db";
import { users, posts, tags, postTags } from "./schema";
import { eq, desc, like, and, sql } from "drizzle-orm";

async function main() {
  // ========== 创建数据 ==========

  // 插入用户
  const [user] = await db
    .insert(users)
    .values({
      name: "张三",
      email: "[email protected]",
    })
    .returning(); // 返回插入的记录

  console.log("创建用户:", user);

  // 批量插入文章
  const insertedPosts = await db
    .insert(posts)
    .values([
      {
        title: "Drizzle ORM 入门指南",
        content: "Drizzle 是一个 TypeScript-first 的 ORM...",
        slug: "drizzle-orm-guide",
        published: true,
        authorId: user.id,
      },
      {
        title: "TypeScript 类型体操",
        content: "深入理解 TypeScript 高级类型...",
        slug: "typescript-type-gymnastics",
        published: false,
        authorId: user.id,
      },
    ])
    .returning();

  console.log(`创建了 ${insertedPosts.length} 篇文章`);

  // ========== 查询数据 ==========

  // 查询所有已发布文章(带作者信息)
  const publishedPosts = await db
    .select({
      id: posts.id,
      title: posts.title,
      slug: posts.slug,
      authorName: users.name,
      authorEmail: users.email,
    })
    .from(posts)
    .innerJoin(users, eq(posts.authorId, users.id))
    .where(eq(posts.published, true))
    .orderBy(desc(posts.createdAt));

  console.log("已发布文章:", publishedPosts);

  // 条件查询:模糊搜索
  const searchResults = await db
    .select()
    .from(posts)
    .where(like(posts.title, "%TypeScript%"));

  console.log("搜索结果:", searchResults);

  // 聚合查询:统计每个用户的发布数量
  const postCounts = await db
    .select({
      authorId: posts.authorId,
      authorName: users.name,
      count: sql<number>`count(*)`.as("post_count"),
    })
    .from(posts)
    .innerJoin(users, eq(posts.authorId, users.id))
    .groupBy(posts.authorId);

  console.log("发布统计:", postCounts);

  // ========== 更新数据 ==========

  // 更新单条记录
  await db
    .update(posts)
    .set({
      published: true,
      updatedAt: new Date(),
    })
    .where(eq(posts.slug, "typescript-type-gymnastics"));

  // 批量更新
  await db
    .update(posts)
    .set({ updatedAt: new Date() })
    .where(and(eq(posts.published, true), eq(posts.authorId, user.id)));

  // ========== 删除数据 ==========

  // 删除单条记录
  await db.delete(posts).where(eq(posts.slug, "drizzle-orm-guide"));

  // 级联删除(用户删除后文章自动删除)
  await db.delete(users).where(eq(users.id, user.id));

  console.log("CRUD 操作完成!");
}

main().catch(console.error);

运行项目:

1
npm run dev

事务处理

Drizzle 支持两种事务模式,适合不同的使用场景:

 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
import { db } from "./db";
import { users, posts } from "./schema";

// 方式一:回调事务(推荐)
await db.transaction(async (tx) => {
  const [user] = await tx
    .insert(users)
    .values({ name: "李四", email: "[email protected]" })
    .returning();

  await tx.insert(posts).values({
    title: "事务处理指南",
    content: "Drizzle 支持强大的事务处理...",
    slug: "transaction-guide",
    authorId: user.id,
  });

  // 如果抛出异常,整个事务会回滚
});

// 方式二:手动事务控制
const tx = db.transaction();
try {
  await tx.insert(users).values({ name: "王五", email: "[email protected]" });
  await tx.commit();
} catch (error) {
  await tx.rollback();
  throw error;
}

Next.js 集成

在 Next.js App Router 中使用 Drizzle ORM 非常自然。创建 lib/db.ts

1
2
3
4
5
6
import { drizzle } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3";
import * as schema from "@/schema";

const sqlite = new Database(process.env.DATABASE_URL || "./local.db");
export const db = drizzle(sqlite, { schema });

API Route 示例 (app/api/posts/route.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
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { posts } from "@/schema";
import { desc, eq } from "drizzle-orm";

export async function GET() {
  const allPosts = await db
    .select()
    .from(posts)
    .where(eq(posts.published, true))
    .orderBy(desc(posts.createdAt));

  return NextResponse.json(allPosts);
}

export async function POST(request: Request) {
  const body = await request.json();

  const [newPost] = await db
    .insert(posts)
    .values({
      title: body.title,
      content: body.content,
      slug: body.slug,
      authorId: body.authorId,
    })
    .returning();

  return NextResponse.json(newPost, { status: 201 });
}

Server Component 示例 (app/posts/page.tsx):

 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
import { db } from "@/lib/db";
import { posts, users } from "@/schema";
import { eq, desc } from "drizzle-orm";

export default async function PostsPage() {
  // 在 Server Component 中直接查询数据库
  const allPosts = await db
    .select({
      id: posts.id,
      title: posts.title,
      slug: posts.slug,
      authorName: users.name,
    })
    .from(posts)
    .innerJoin(users, eq(posts.authorId, users.id))
    .where(eq(posts.published, true))
    .orderBy(desc(posts.createdAt));

  return (
    <div>
      <h1>博客文章</h1>
      {allPosts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>作者: {post.authorName}</p>
        </article>
      ))}
    </div>
  );
}

性能对比

Drizzle ORM 相比其他 ORM 的性能优势:

指标 Drizzle Prisma TypeORM
冷启动时间 ~5ms ~200ms ~150ms
包体积 ~50KB ~5MB+ ~2MB
类型推断 编译时 运行时 运行时
SQL 控制 完全 部分 部分
学习曲线 低(熟悉SQL)

Drizzle 的冷启动时间特别适合 Serverless 和边缘计算场景,这也是它在 Vercel、Cloudflare Workers 社区迅速流行的原因。

最佳实践

  1. Schema 即文档:把 Schema 文件当作数据库文档,字段名、类型、约束一目了然
  2. 善用 returning():插入和更新时使用 returning() 获取操作结果,避免额外查询
  3. 合理使用事务:涉及多表操作时务必使用事务,保证数据一致性
  4. 利用 TypeScript 类型:Drizzle 会自动推断查询结果类型,充分利用 IDE 的类型提示
  5. 开发环境用 push,生产环境用 migratedrizzle-kit push 适合快速迭代,migrate 适合正式环境的版本控制

总结

Drizzle ORM 代表了 TypeScript 数据库访问层的新方向:零运行时开销、端到端类型安全、SQL-like 语法。它特别适合:

  • Next.js / Remix 等全栈框架:Server Component 中直接查询数据库
  • Serverless / 边缘计算:冷启动时间极短
  • 类型安全优先的团队:编译时捕获数据库错误
  • 熟悉 SQL 的开发者:无需学习新的查询语言

如果你正在寻找一个轻量、高效、类型安全的 TypeScript ORM,Drizzle ORM 绝对值得一试。