what

1.5K
0
0
最后修改于

不管是什么 ORM(哪怕是以 Drizzle 为代表的新型 ORM),作为一个合格的 ORM,通常都包含以下四大核心能力:

  • Schema 定义 (Schema Definition)

    • 概念:用代码来描述数据库的表长什么样。让代码成为 “单一事实来源 (Single Source of Truth)”。
    • Drizzle 里的样子:就是上面定义的 pgTable,你修改了 TS 代码,就等同于修改了对数据库结构的定义。
  • ② 查询构建器 (Query Builder)

    • 概念:开发者不需要手写拼接 SQL 字符串(比如 SELECT * FROM users WHERE id = + id),而是通过调用代码函数来生成 SQL。这能防止 SQL 注入,并且享受代码编辑器的代码补全功能。
    • Drizzle 里的样子:db.select().from(users).where(eq (users.id, 1))。ORM 会在底层自动把它翻译成对应的 SQL 语句发给数据库。
  • ③ 数据结果转换 (Data Mapping / Hydration)

    • 概念:数据库返回的原本是干巴巴的二维表格数据(Row),ORM 会自动把它组装、转换为你在代码里能直接使用的对象或数组(Object / Array)。
    • Drizzle 里的样子:查询完成后,你拿到的直接就是一个符合你 TypeScript 类型的对象数组:[{ id: 1, name: "Alice" }],而不是一串原始的数据库 buffer。
  • ④ 数据库迁移管理 (Migrations)

    • 概念:随着业务发展,你要加字段、删表。ORM 通常会提供工具,把你代码结构的变化,自动生成对应的 SQL 变更脚本(ALTER TABLE...),并帮你记录哪些变更已经执行过了。
    • Drizzle 里的样子:配套的 drizzle-kit 工具,可以自动比对你当前的 TS 代码和旧版本的差异,生成 .sql 迁移文件。

1. 架构分层设计:从代码到 SQL 的生命周期#

任何一个成熟的 ORM,内部都有严格的分层设计。为什么必须分层?因为要解耦 “开发者怎么写代码”、“怎么生成 SQL” 和 “怎么和具体的数据库(MySQL / PG)通信”。

  • **API 层 **
    • SQL Like API:提供给开发者使用的链式调用(Method Chaining)接口。比如 .select().from().where()。这种 API 设计直观清晰
    • Relation API:
  • AST 层
    API 层收集到的意图,会被转换成一个 JSON 格式的 AST 树结构。在这个阶段可以用 AST 可以方便地进行静态分析、防止 SQL 注入、并且可以动态修改查询逻辑。
  • 方言翻译层 (Dialect / SQL Compiler)
    拿到 AST 后,根据你连接的数据库类型,将其 “编译” 成特定的 SQL 字符串。
  • 驱动与映射层 (Driver & Hydration)
    调用底层驱动(如 mysql2pg)执行最终 SQL,拿到扁平的行列数据,反序列化成代码中的嵌套对象。

2. 两大经典流派:实体(Entity)应该如何设计?#

在 ORM 的历史长河中,针对 “数据模型怎么映射”,演化出了两种根本性的设计模式(由 Martin Fowler 提出),这也是决定一个 ORM API 长什么样的核心:

流派 A:Active Record(活动记录模式)#

  • 概念:数据和操作数据的行为(CRUD)绑定在一起。模型(Model)不仅代表数据,还包含了 save(), delete() 等方法。
  • API 表现
    // TypeORM 的典型用法
    const user = new User();
    user.name = "Alice";
    await user.save(); // 对象自己保存自己
    
  • 为什么这样设计:极度符合直觉,开发极快。
  • 缺点:违反了单一职责原则(SRP)。对象变得极其臃肿,而且很难处理复杂的跨表联查。

流派 B:Data Mapper(数据映射器模式)#

  • 概念:实体(Entity)只是纯粹的数据结构(贫血模型),没有任何操作数据库的能力。所有数据库操作交由一个独立的 Mapper / Repository 来执行。
  • API 表现
    // Drizzle 的典型用法
    const newUser = { name: "Alice" }; // 纯数据
    await db.insert(users).values(newUser); // db 是独立的 Mapper
    
  • 为什么这样设计:极致的解耦。模型和数据库操作彻底分离。Drizzle 就是坚定的 Data Mapper 拥趸(甚至可以说是 Table Data Gateway 模式)。它认为数据库操作就应该是独立的函数,而不是对象上的方法。

3. Schema 定义机制:如何用代码描述表结构?#

ORM 必须知道数据库长什么样,这就需要定义 Schema。Schema 的 API 设计目前有三个演进方向

  • 基于装饰器 (Decorators) + 类 (Class)
    如 TypeORM (@Column, @Entity),利用元编程(Reflect Metadata)在运行时获取类型信息。但是 TS 的装饰器生态非常割裂(旧版和标准版冲突),且带来了运行时的额外开销。
  • 基于 DSL
    如 Prisma (schema.prisma 文件),为了实现跨语言支持(写一份 DSL,可以生成 TS, Go, Rust 的客户端),并且可读性极高。但是需要额外的编译步骤(prisma generate),开发者失去了对底层的控制力。
  • 基于纯函数与字面量推导 (Functional & Inference)
    如 Drizzle,利用 TypeScript 强大的类型推导能力。不需要额外的编译,不需要装饰器。一个简单的 JS 函数调用,底层利用泛型直接锁定了表的结构。
const users = pgTable('users', { id: serial('id') });

4. 关系映射与 N + 1 问题#

ORM 最大的挑战不在于单表查询,而在于关系(1 对 1, 1 对多, 多对多)的处理

  • N + 1 问题
    假如要查 10 个用户,并且连带查出他们发表的文章。如果 ORM 设计不好,会先发 1 条 SQL 查出 10 个用户,然后针对每个用户循环发 10 条 SQL 去查文章。这会显著拖慢性能
    为了解决这个问题,往往需要用户通过 API 显式声明要连什么一起查。如下:
db.query.users.findMany({
	with: { posts: true } // 显式声明预加载
});

通过强制要求显式声明查询关联关系,然后在底层使用一个复杂的 JOIN 或者 WHERE IN (...) 将其优化为 1~2 条 SQL。