全书目录

第五十六篇:Drizzle ORM 完全指南 —— 类型化数据库制图局宇宙

第五十六篇:Drizzle ORM 完全指南 —— 类型化数据库制图局宇宙

20 分钟 8,568 字 第 962 / 962 个阅读单元

Drizzle 最适合用一句话理解:

它不是帮你“忘掉 SQL”的 ORM,而是给你一套带比例尺、坐标系、图例、道路编号的数据库地图系统。你仍然在地图上行走,但不会再闭眼走路。

如果把数据库开发想成城市建设,那么:

text
数据库表         = 城市地块
schema          = 城市总平面图
migration       = 施工变更单
query builder   = 规划路线的尺子和图钉
relations       = 地图上的道路连通关系
raw SQL         = 直接看经纬度自己开车

Drizzle 的价值,不在“替你做决定”,而在“让你每一步都看得见,而且类型系统跟着你走”。


#一、先把 Drizzle 放到正确坐标系里

很多人第一次看 Drizzle,会问一句:

“它到底是 ORM,还是 SQL builder?”

正确答案是:它两边都占,但重心明显偏向“类型化 SQL builder”。

可以把常见方案排成一条线:

text
纯 SQL  <------  Drizzle  ------>  重抽象 ORM
控制力高                          自动化高
魔法少                            魔法多
SQL感强                           框架感强

再具体一点:

text
Raw SQL
  你自己写 SQL、自己维护类型、自己做结果映射

Drizzle
  你用 TypeScript 声明 schema,用接近 SQL 的方式写查询,
  类型跟着 schema 和 select 结果走

Prisma
  你先写 Prisma 自己的 schema DSL,再生成 Prisma Client,
  查询体验更像“操作模型对象”

TypeORM
  你先写实体类和装饰器,依赖运行时元数据,
  查询可以走 repository,也可以转去 QueryBuilder

所以 Drizzle 的核心心智不是“对象世界替代 SQL 世界”,而是:

text
SQL 仍然是世界本体
TypeScript 负责把 SQL 世界精确投影到代码里
Drizzle 就是那台投影仪

这也是为什么官方一直强调它是贴近 SQL 的、薄的一层 typed layer。


#二、Drizzle 的真正心智:不是“会不会 ORM”,而是“会不会画图”

理解 Drizzle,最重要的是这张图:

text
                ┌──────────────────────┐
                │   TypeScript Schema  │
                │  表/列/约束/索引定义   │
                └──────────┬───────────┘
                           │
                  推导类型 │ 生成迁移/对比差异
                           │
          ┌────────────────┴──────────────┐
          │                               │
┌─────────▼─────────┐           ┌─────────▼─────────┐
│  Query Builder     │           │   Drizzle Kit     │
│ select/insert/...  │           │ generate/push/... │
└─────────┬─────────┘           └─────────┬─────────┘
          │                               │
          └──────────────┬────────────────┘
                         │
                 ┌───────▼───────┐
                 │   SQL 数据库   │
                 └───────────────┘

这张图说明了 4 个关键事实:

  1. Drizzle 不是把数据库藏起来,而是把数据库结构显式写进 TypeScript。
  2. 类型不是凭空来的,而是从 schema 和 query shape 推导来的。
  3. migration 不是黑盒魔法,而是 schema 变化的 SQL 落地过程。
  4. 你随时可以退回 SQL,不会被框架关进笼子里。

所以,Drizzle 的学习重点不是“记住多少 API”,而是建立这套判断:

  • 结构定义写在 schema。
  • 结构演进走 migration。
  • 常规查询走 SQL-like builder。
  • 关系嵌套走 relations/query API。
  • builder 表达不了的地方,用 sql`` 补进去。

这套分工一旦清楚,Drizzle 就会非常顺手。


#三、Schema-first 与 TS-first,到底是什么意思

很多人会把这两个词混着用,但在 Drizzle 里最好分开理解。

#1. Schema-first:先想结构,不先想对象

在 Drizzle 里,数据库设计的起点是表、列、主键、外键、索引、约束。

不是先问:

“我有一个 User 类,它有哪些方法?”

而是先问:

“数据库里到底有哪些表?列是什么?关系如何落地?”

这是非常 SQL 的思维。

#2. TS-first:这份结构图写在 TypeScript 里

Drizzle 的特别之处在于,它没有再发明一套独立 DSL 来写 schema,而是直接让你在 TypeScript 里声明数据库结构。

也就是:

text
Schema-first:先定义数据库结构
TS-first:这份结构定义直接写在 TypeScript 中

所以 Drizzle 本质上是:

text
Schema-first inside TypeScript
在 TypeScript 里做 schema-first

这和 Prisma 很不一样。

  • Prisma:schema.prisma 是独立 DSL,之后生成 Client
  • Drizzle:schema.ts 本身就是源码的一部分,直接参与类型推导和迁移流程

#四、先看一个完整 schema:你会立刻明白 Drizzle 的气质

下面用 PostgreSQL 举例。

ts
// src/db/schema.ts
import {
  pgTable,
  serial,
  text,
  integer,
  timestamp,
  primaryKey,
} from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";

export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
  createdAt: timestamp("created_at", { withTimezone: true })
    .defaultNow()
    .notNull(),
});

export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  authorId: integer("author_id")
    .notNull()
    .references(() => users.id, { onDelete: "cascade" }),
  title: text("title").notNull(),
  body: text("body").notNull(),
  createdAt: timestamp("created_at", { withTimezone: true })
    .defaultNow()
    .notNull(),
});

export const comments = pgTable("comments", {
  id: serial("id").primaryKey(),
  postId: integer("post_id")
    .notNull()
    .references(() => posts.id, { onDelete: "cascade" }),
  authorId: integer("author_id")
    .notNull()
    .references(() => users.id, { onDelete: "cascade" }),
  content: text("content").notNull(),
  createdAt: timestamp("created_at", { withTimezone: true })
    .defaultNow()
    .notNull(),
});

export const tags = pgTable("tags", {
  id: serial("id").primaryKey(),
  name: text("name").notNull().unique(),
});

export const postsToTags = pgTable(
  "posts_to_tags",
  {
    postId: integer("post_id")
      .notNull()
      .references(() => posts.id, { onDelete: "cascade" }),
    tagId: integer("tag_id")
      .notNull()
      .references(() => tags.id, { onDelete: "cascade" }),
  },
  (t) => [primaryKey({ columns: [t.postId, t.tagId] })]
);

export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
  comments: many(comments),
}));

export const postsRelations = relations(posts, ({ one, many }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
  comments: many(comments),
  postsToTags: many(postsToTags),
}));

export const commentsRelations = relations(comments, ({ one }) => ({
  author: one(users, {
    fields: [comments.authorId],
    references: [users.id],
  }),
  post: one(posts, {
    fields: [comments.postId],
    references: [posts.id],
  }),
}));

export const tagsRelations = relations(tags, ({ many }) => ({
  postsToTags: many(postsToTags),
}));

export const postsToTagsRelations = relations(postsToTags, ({ one }) => ({
  post: one(posts, {
    fields: [postsToTags.postId],
    references: [posts.id],
  }),
  tag: one(tags, {
    fields: [postsToTags.tagId],
    references: [tags.id],
  }),
}));

这段代码很能说明 Drizzle 的风格:

  • 表结构是显式的。
  • 外键是显式的。
  • 多对多不会帮你偷偷建中间表,必须自己定义。
  • relation 不是神秘黑盒,而是你明确告诉框架“路怎么连”。

这就是“地图派”风格。


#五、Schema 文件怎么组织,才像工程而不是样例

官方允许你把 schema 放一个文件,也允许拆成多个文件。真正工程里,常见做法是按领域拆分:

text
src/
  db/
    schema/
      users.ts
      posts.ts
      comments.ts
      tags.ts
      index.ts

关键原则只有一个:

Drizzle Kit 需要能拿到所有导出的模型。

也就是说,你拆文件没问题,但要保证表、枚举、视图、关系等都被正确导出,否则 migration diff 会不完整。

如果你想在代码里用驼峰、数据库里用蛇形,也可以在初始化时做 casing 映射:

ts
// src/db/client.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "./schema";

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

export const db = drizzle(pool, {
  schema,
  casing: "snake_case",
});

这个细节很实用,因为它能让你少写很多手动列别名。


#六、Migration:Drizzle 不是只有一条路,而是三条路

Drizzle Kit 最值得讲透的地方,不是命令本身,而是它允许三种迁移哲学并存。

text
1. generate + migrate
   TS schema --> SQL migration files --> apply to DB

2. push
   TS schema --> diff --> direct apply to DB

3. pull
   existing DB --> introspect --> schema.ts

#1. generate + migrate:最像正统工程流

ts
// drizzle.config.ts
import "dotenv/config";
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  dialect: "postgresql",
  schema: "./src/db/schema.ts",
  out: "./drizzle",
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});
bash
npx drizzle-kit generate
npx drizzle-kit migrate

这条路的心智是:

  • TypeScript schema 是源码里的地图底稿
  • generate 负责产出 SQL 变更单
  • migrate 负责把变更单应用到数据库

这通常最适合团队协作,因为 SQL 文件会进入版本库,变更历史清晰。

#2. push:更像快速施工

bash
npx drizzle-kit push

push 的机制是:

  • 读你的 schema
  • 反查数据库当前结构
  • 做 diff
  • 直接应用 SQL

它非常适合原型、个人项目、早期开发。

工程上的判断是:

  • 开发期、试验期:push 很爽
  • 多人协作、审计要求高、生产控制严格:通常更偏向 generate + migrate

这句是工程判断,但非常重要,因为 migration 从来不只是“能不能改表”,而是“变更历史是否可追踪”。

#3. pull:接管已有数据库

如果你不是从零开始,而是接一个老库,Drizzle 也支持 database-first:

bash
npx drizzle-kit pull

它会把数据库现状拉回成 schema.ts,让你的 TypeScript 世界和数据库世界对齐。

这点很关键,因为很多 ORM 一上来就假设“必须从我开始建库”,而 Drizzle 明确支持两种现实:

  • codebase-first
  • database-first

换句话说,它承认真实世界不总是新项目。

#4. 自定义迁移:别把工具能力当成边界

如果某些 DDL 变更或数据修复不适合自动生成,Drizzle Kit 支持自定义空迁移文件:

bash
npx drizzle-kit generate --custom --name=seed-users

这说明 Drizzle 的设计立场很清楚:

“自动化是默认选项,但最终控制权留给你。”


#七、查询:Drizzle 最像 SQL 的地方,恰恰是它最强的地方

很多 ORM 的问题是:简单查询很顺手,一复杂就要么类型丢失,要么被迫写字符串 SQL。

Drizzle 的思路是:

  • 常规查询,用 SQL-like builder
  • 关系嵌套,用 query/relations API
  • 特殊表达式,用 sql``

这三层拼起来,覆盖面非常完整。


#八、先掌握 SQL-like 查询:把它当“带类型的 SQL”

#1. Insert

ts
const createdUser = await db
  .insert(users)
  .values({
    name: "Lin",
    email: "lin@example.com",
  })
  .returning();

你会发现它几乎就是 SQL 的顺序:

text
insert into users ... values ... returning ...

#2. Select

ts
import { eq, desc } from "drizzle-orm";

const latestPosts = await db
  .select({
    id: posts.id,
    title: posts.title,
    authorName: users.name,
    createdAt: posts.createdAt,
  })
  .from(posts)
  .leftJoin(users, eq(posts.authorId, users.id))
  .orderBy(desc(posts.createdAt))
  .limit(10);

注意这里一个非常关键的 Drizzle 特征:

select 里写出来的 shape,基本就是结果类型的 shape。

也就是说,你选什么,类型就长什么样。

#3. Update

ts
const updated = await db
  .update(posts)
  .set({ title: "Drizzle ORM 深入理解" })
  .where(eq(posts.id, 1))
  .returning();

#4. Delete

ts
const deleted = await db
  .delete(comments)
  .where(eq(comments.id, 9))
  .returning();

这套 API 的好处,是 SQL 心智完全不需要重建。

你不是在学习一种“框架专属查询语言”,你是在写被 TypeScript 扶住的 SQL。


#九、聚合、表达式、原生能力补位:sql`` 是 Drizzle 的逃生舱,也是增强器

真实项目里最怕一种 ORM:80% 能写,剩下 20% 只能逃回字符串 SQL,而且类型和参数化一起丢掉。

Drizzle 的 sql`` 恰好就是为这个问题准备的。

ts
import { sql } from "drizzle-orm";

const stats = await db
  .select({
    authorId: posts.authorId,
    postCount: sql<number>`count(${posts.id})`.mapWith(Number),
  })
  .from(posts)
  .groupBy(posts.authorId);

或者直接执行原生 SQL:

ts
const userId = 42;

const result = await db.execute(
  sql`select * from ${users} where ${users.id} = ${userId}`
);

这里有两个重点:

  1. 表名、列名会被安全映射。
  2. 动态值会走参数化,不是字符串拼接。

所以 sql`` 不是“放弃类型”,而是“在保留参数化和部分类型辅助的前提下,补足 SQL 表达力”。

这也是 Drizzle 和“纯 ORM 封装派”最大的不同之一。


#十、Relations:它不是数据库魔法,而是应用层的连线图

这是 Drizzle 最容易被误解的地方。

官方文档里专门强调了一点:

relationsforeign keys 不是一回事。

可以这样记:

text
foreign key = 数据库层的硬约束
relations   = 应用层的导航关系

更像地图的话:

text
foreign key = 真实存在的道路护栏
relations   = 导航软件里的可通行路线

两者经常一起出现,但不是同一个层面。

#1. 一对多

text
users (1)  --------<  posts (n)
ts
export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));

#2. 多对多

text
posts (n)  ----< posts_to_tags >----  tags (n)

多对多在 Drizzle 里必须显式经过中间表。这个设计很“数据库本位”,但工程上反而更稳,因为中间表往往迟早会长出额外字段,比如:

  • 排序
  • 权重
  • 创建时间
  • 创建人

也就是说,Drizzle 不是把多对多“藏起来”,而是提醒你:数据库里它本来就不是空气。


#十一、关系查询:什么时候该用 db.query.*

当你只是要一张扁平结果表,优先用 core builder。

当你要嵌套结构,比如“文章带作者,评论再带评论作者”,就很适合 relations 查询。

ts
const feed = await db.query.posts.findMany({
  with: {
    author: true,
    comments: {
      with: {
        author: true,
      },
    },
  },
});

它返回的就是嵌套对象结构,不需要你手工把 join 结果再做一次 regroup。

这层 API 的意义不是“替代 SQL-like 查询”,而是:

“当目标结果本来就是嵌套 JSON 结构时,别再手工拼装了。”

所以一个很实用的判断标准是:

  • 要扁平表结果、聚合、复杂 join:core builder
  • 要嵌套对象树:relations/query API

二者不是竞争关系,是分工关系。


#十二、一个完整例子:列表、详情、统计,各用哪种写法

#1. 列表页:扁平化查询优先

ts
import { desc, eq } from "drizzle-orm";

const list = await db
  .select({
    postId: posts.id,
    title: posts.title,
    authorName: users.name,
    createdAt: posts.createdAt,
  })
  .from(posts)
  .innerJoin(users, eq(posts.authorId, users.id))
  .orderBy(desc(posts.createdAt))
  .limit(20);

这是典型“SQL builder 擅长场景”。

#2. 详情页:嵌套结构优先

ts
const detail = await db.query.posts.findFirst({
  with: {
    author: true,
    comments: {
      with: {
        author: true,
      },
    },
  },
});

这是典型“relations 擅长场景”。

#3. 统计页:聚合优先,回到 core builder

ts
import { sql } from "drizzle-orm";

const topAuthors = await db
  .select({
    authorId: users.id,
    authorName: users.name,
    postCount: sql<number>`count(${posts.id})`.mapWith(Number),
  })
  .from(users)
  .leftJoin(posts, eq(posts.authorId, users.id))
  .groupBy(users.id, users.name)
  .orderBy(sql`count(${posts.id}) desc`);

这就是最典型的 Drizzle 使用方式:

text
列表 -> SQL-like builder
详情 -> relations query
统计 -> builder + sql

不是“全都用一种写法”,而是“按目标结果选武器”。


#十三、动态查询:Drizzle 为什么默认不让你重复 .where()

Drizzle 有一个很有意思的设计:

默认情况下,很多 query method 只能调用一次。

比如:

ts
const query = db
  .select()
  .from(users)
  .where(eq(users.id, 1))
  .where(eq(users.name, "Lin")); // 类型报错

这不是故意为难你,而是因为 SQL 语义里本来就只有一个 WHERE 子句。Drizzle 宁可在类型层面阻止歧义,也不帮你偷偷“猜测并合并”。

如果你确实要动态拼查询,可以进入动态模式:

ts
function withPagination<T extends typeof baseQuery>(qb: T, page = 1, pageSize = 10) {
  return qb.limit(pageSize).offset((page - 1) * pageSize);
}

const baseQuery = db.select().from(users).$dynamic();
const pagedQuery = withPagination(baseQuery, 2, 20);

这背后体现的还是同一个哲学:

“默认尊重 SQL 本体,动态拼装时再明确切换模式。”


#十四、和 Prisma、TypeORM、SQL 直写,到底怎么比

#1. 总览表

方案 结构定义方式 查询体验 类型来源 迁移心智 代价
Drizzle schema.ts 贴近 SQL,另有关系查询 schema + select shape generate / migrate / push / pull 多路线 需要懂 SQL 和数据库设计
Prisma schema.prisma 更像操作模型客户端 生成的 Prisma Client 围绕 Prisma schema 与 migrate 抽象更高,离 SQL 更远
TypeORM 实体类 + 装饰器 repository + QueryBuilder 运行时元数据 + TS 围绕 entity/migration 容易在复杂查询时切到另一套心智
Raw SQL .sql 或字符串 直接 SQL 你自己维护 你自己维护 控制最大,重复劳动也最大

#2. Drizzle vs Prisma

Prisma 的强项是:

  • 上手体验统一
  • include、嵌套写法直观
  • Client 生成后类型体验很完整

Drizzle 相比 Prisma 的差异,不是“类型更强”这么简单,而是“离 SQL 更近”。

可以这样记:

text
Prisma:先写 Prisma 世界,再生成访问数据库的客户端
Drizzle:直接在 TypeScript 里画数据库地图,然后沿着 SQL 走

所以如果你的团队希望:

  • 开发者尽量少碰 SQL
  • 更喜欢模型式客户端
  • 接受独立 schema DSL

那 Prisma 会更顺手。

如果你的团队希望:

  • SQL 就是主语言
  • 类型必须跟 SQL 结果形状紧贴
  • 不想被框架 DSL 和生成物强绑定

Drizzle 会更合拍。

#3. Drizzle vs TypeORM

TypeORM 的典型心智是实体类、装饰器、关系装饰、repository、query builder。

它很像在说:

“先把数据库世界翻译成面向对象实体。”

Drizzle 则像在说:

“别急着翻译,先把数据库世界准确画出来。”

两者最大的差异点在这里:

  • TypeORM 更偏 entity-first、metadata-first
  • Drizzle 更偏 schema-first、SQL-first、type-first

复杂查询时,这种差异会尤其明显。

TypeORM 官方文档自己也区分了:

  • 简单查找用 find*
  • 更复杂的查询转 QueryBuilder

而 Drizzle 从一开始就把“SQL-like builder 是主战场”这件事说得很明白,所以心智更统一。

#4. Drizzle vs SQL 直写

这组对比最容易误判。

很多人以为:

“我都懂 SQL 了,为什么不直接写 SQL?”

答案是:当然可以,Drizzle 也不阻止你。

但直写 SQL 的长期成本在于:

  • 查询结果类型要自己维护
  • 表名列名重构时缺少编译期保护
  • join 后结果 shape 容易和调用方脱节
  • 复用与组合能力弱

Drizzle 不是替代 SQL,而是把 SQL 工程化。

所以更准确的说法不是:

“Drizzle 比 SQL 更强。”

而是:

“Drizzle 让 SQL 在 TypeScript 项目里更可维护。”


#十五、最常见的 6 个误区

#误区 1:Drizzle 就是 Prisma 的轻量替代

不是。

Prisma 的核心体验是“生成客户端操作模型”,Drizzle 的核心体验是“用 TypeScript 写 schema,用接近 SQL 的方式查询”。

它们不是同一条设计路线的轻重版本。

#误区 2:定义了 relations 就等于建了外键

不是。

relations 是应用层关系描述,不会自动替你创建数据库外键。外键是否存在,取决于你的表定义和 migration。

#误区 3:用了 Drizzle,就可以不懂 SQL

恰恰相反。

Drizzle 的上限很高,前提是你接受 SQL 是主语。越懂 SQL,越能发挥 Drizzle。

#误区 4:push 一定比 migration 文件更先进

不是。

push 是快速,但不总是更适合团队工程。涉及审计、回滚、评审、环境一致性时,SQL migration 文件通常更稳。

#误区 5:多对多关系应该自动隐藏中间表

不一定。

在真实业务里,中间表经常会长业务字段。Drizzle 让你显式建中间表,反而更贴近长期演化。

#误区 6:聚合结果天然就是 number

不一定。

官方文档明确提醒,某些数据库里 count() 之类的结果可能是 bigintdecimal 语义,需要用 .mapWith(Number) 或相关 API 处理。


#十六、Drizzle 适合什么项目

  • TypeScript 后端项目,尤其是 Node/Bun/Deno/Edge 场景。
  • 团队已经接受 SQL 是核心能力,而不是希望完全躲开 SQL。
  • 你需要精确控制 schema、索引、外键、join、聚合、原生表达式。
  • 你要接管已有数据库,而不是只能从新库开始。
  • 你希望 schema、类型、查询、迁移在同一套源码里收敛。

更具体地说,下面这类团队通常会很喜欢 Drizzle:

  • 后端工程师本来就会写 SQL
  • 代码评审重视 migration 可读性
  • 讨厌过多运行时魔法
  • 希望 query shape 和 TS 类型一一对应

#十七、Drizzle 不太适合什么项目

  • 团队明确不想碰 SQL,只想“像操作对象一样操作数据库”。
  • 项目成员对数据库设计基础较弱,希望框架尽量兜底。
  • 你特别依赖重 ORM 的实体生命周期、深度级联、统一 repository 风格。
  • 项目不以 TypeScript 为中心。
  • 你的核心目标不是“更强控制力”,而是“更强默认抽象”。

一句话概括就是:

text
想让框架替你想很多,Drizzle 可能不够“厚”
想自己想清楚并且要类型系统全程跟随,Drizzle 很合适

#十八、实战选择建议:到底什么时候该选 Drizzle

如果你现在正在做技术选型,可以直接用这张决策图:

text
团队是否接受 SQL 是一等公民?
  否 -> 更偏 Prisma / 更高抽象方案
  是 -> 继续

是否希望 schema 直接写在 TypeScript 中?
  否 -> 更偏 Prisma DSL / 传统 SQL migration 流
  是 -> 继续

是否希望复杂查询依然保留编译期类型约束?
  否 -> Raw SQL 也可以
  是 -> Drizzle 很有竞争力

再说得更实用一点:

  • 你是“数据库地图绘制派”而不是“对象自动驾驶派”时,选 Drizzle。
  • 你想要的是“精确的尺子”,不是“热心但偶尔猜错的管家”时,选 Drizzle。
  • 你愿意理解数据库本体,并希望 TypeScript 成为数据库设计的放大镜时,选 Drizzle。

#十九、总结

Drizzle 最值得学的,不是某个 API 名字,而是这套稳定心智:

text
数据库不是对象仓库,而是结构化地图
schema 是地图底稿
migration 是地图改版记录
query builder 是有类型的量尺
relations 是导航连线
sql`` 是保底也是增强

所以 Drizzle 的本质,不是“帮你忘掉 SQL”,而是“帮你把 SQL、类型系统、迁移历史,画进同一张工程地图”。

你一旦接受这件事,它就会非常顺。

你一旦期待它像重 ORM 那样替你藏起数据库,它反而会显得“不够热情”。

这不是缺点,这是它的设计立场。

而这也正是 Drizzle 最强的地方:
它让数据库开发回到本体,同时把 TypeScript 的编译期能力,真正接到了数据库这张地图上。

#官方来源链接清单