查询场景

1.7K
0
0
最后修改于

1. 分页 (Pagination)#

分页是所有 C 端 / B 端系统的标配,但在数据库底层和 ORM 设计上,存在两条截然不同的路线:

A 路线:Offset / Limit(偏移量分页)#

  • 概念:最传统的分页。告诉数据库 “跳过前 M 条,取 N 条”。
  • API 设计:绝大多数 ORM 都直接映射了这两个关键字。
  • 痛点与为什么这样设计
    • 优点:实现极简,可以随意跳转页码(比如直接跳到第 100 页)。
    • 致命缺陷(深度翻页性能衰退):当 offset(1000000) 时,数据库实际上需要扫描 1000010 行,然后丢弃前一百万行。数据量极大时性能直接崩溃。

B 路线:Cursor-based(游标分页 / Keyset 分页)#

  • 概念:利用上一页最后一条记录的唯一标识(通常是自增 ID 或时间戳)作为 “游标”,告诉数据库 “取 ID > 游标 的下 N 条”。
  • API 设计:这需要 ORM 的动态 where 构建配合。
    // 假设拿到了上一页最后一条记录的游标 lastId = 50
    db.select()
      .from(users)
      .where(gt(users.id, 50)) // 核心:使用索引进行比较
      .orderBy(asc(users.id))
      .limit(10);
    

现代应用(如移动端无限下拉加载)通常不需要 “跳页”,游标分页完美利用了数据库索引(B+ 树),无论翻到第几页,查询速度都是 O(1)O(1) 级别。Prisma 等现代 ORM 甚至提供了专门的 cursor 抽象 API,而 Drizzle 则倾向于让你显式手写游标的 where 条件。

2. 动态查询构建 (Dynamic Query):静态类型的噩梦#

这是 ORM 必须跨越的一道坎。前端传来的搜索条件是动态的(用户可能按名字搜,也可能按时间搜,或者都不传)。

  • 挑战:如何在保持 TypeScript 类型安全的前提下,优雅地拼接 WHERE 条件?如果前端传了 undefined,ORM 怎么处理?
  • 对象式 ORM 的解法(如 Prisma)
    它们通常接受一个深度嵌套的对象,并且在底层设计上会自动忽略 undefined 的字段。
    // Prisma 风格:极其优雅,但黑盒
    db.user.findMany({
      where: {
        name: req.query.name || undefined, 
        age: req.query.age ? { gt: req.query.age } : undefined
      }
    });
    
  • SQL Builder 式 ORM 的解法(如 Drizzle)
    因为它强调 “像写 SQL 一样”,所以它提供了一个数组收集器(Array 模式)配合布尔逻辑操作符(and, or)。Drizzle 的聪明之处在于,它的 and() 函数会自动过滤掉 undefined 的条件。
    // Drizzle 的动态构建模式
    const filters = [];
    
    if (req.query.name) {
      filters.push(ilike(users.name, `%${req.query.name}%`));
    }
    if (req.query.status) {
      filters.push(eq(users.status, req.query.status));
    }
    
    db.select().from(users).where(and(...filters)); 
    // 如果 filters 为空,and() 会安全地被忽略
    

3. 排序构建 (Dynamic Sorting):防范注入与映射转换#

  • 挑战:前端传来的排序规则通常是字符串形式(如 sort=created_at,desc),但如果直接把前端传来的字符串拼进 SQL,会引发极高风险的 SQL 注入
  • API 设计与解决方案
    ORM 必须强制开发者通过一个 “映射表” 或 “验证机制”,将外部字符串转换为 ORM 内部安全的列引用(Column Reference)。
    // Drizzle 动态排序的典型处理范式
    const sortField = req.query.sortBy; // 比如 "age"
    const sortOrder = req.query.order === 'desc' ? desc : asc;
    
    // 建立一个安全白名单映射,防止注入非表字段
    const columnMap = {
      age: users.age,
      createdAt: users.createdAt
    };
    
    const targetColumn = columnMap[sortField] || users.id; // 默认回退到 id
    
    db.select().from(users).orderBy(sortOrder(targetColumn));
    

4. 批量写入与更新 (Batch / Bulk & Upsert)#

批量写入 (Bulk Insert)#

  • 痛点:如果用一个 for 循环执行 1000 次单条 INSERT,会导致 1000 次网络 IO(甚至开启 1000 个事务),性能极差。
  • API 设计:几乎所有 ORM 的 insert 方法都支持接收数组,在底层将它们编译为一条 INSERT INTO table VALUES (...), (...), (...) 语句。
    db.insert(users).values([{ name: "A" }, { name: "B" }, { name: "C" }]);
    

冲突处理 (Upsert / ON CONFLICT)#

  • 概念:“有则更新,无则插入”。这在同步外部数据源时极其常用。
  • API 设计挑战:各个数据库底层语法完全不同(MySQL 用 ON DUPLICATE KEY UPDATE,PostgreSQL 用 ON CONFLICT DO UPDATE)。ORM 需要抹平这种方言差异。
    // Drizzle 为 PostgreSQL 设计的 Upsert API
    db.insert(users)
      .values({ id: 1, name: "New Name" })
      .onConflictDoUpdate({ 
        target: users.id, // 冲突依据(必须是唯一索引/主键)
        set: { name: "New Name" } // 冲突后执行的更新
      });
    

5. 迁移方案 (Migrations):数据库演进的工程化#

这不仅是代码层面的事,更是运维和工程化(DevOps)的核心。ORM 中的 Migration 设计,决定了团队协作时数据库会不会 “被搞炸”。

  • 现代 ORM 迁移的核心原理(状态对比算法 / Diffing)
    1. 目标态:你刚刚在代码里修改的 Schema(如 age: integer('age'))。
    2. 当前态:数据库现在的结构(ORM 通常会在本地维护一个包含结构快照的 JSON 文件或读取线上元数据)。
    3. ORM 的 CLI 工具(如 drizzle-kit generate)会对两者进行抽象语法树(AST)级别的 Diff 对比。
    4. 自动生成增量的 SQL 语句:ALTER TABLE users ADD COLUMN age INTEGER;
  • 为什么必须生成 .sql 文件?
    • 在早期的某些 ORM 中(如早期的 Sequelize 自动同步),ORM 会在运行时自动去改生产数据库的结构(Auto Sync)。这在生产环境是灾难级的,容易导致锁表或数据丢失。
    • 现代最佳实践(Drizzle / Prisma 都在遵守):代码只负责生成 .sql 脚本。生成的脚本必须经过人工 Code Review(确认没有执行如 DROP TABLE 这种危险动作),然后在 CI / CD 阶段由迁移引擎按顺序(通过时间戳命名,如 0001_add_age.sql)依次应用到数据库中。并且会在数据库内建立一张特殊的表(如 __drizzle_migrations)记录哪些脚本已经执行过。

总结:如何评估一个 ORM 的实战能力?#

当你把这些概念纳入知识库后,你评判一个 ORM 是否优秀的标准就不再只是 “语法帅不帅” 了,而是要看:

  1. 它能否平滑地处理 undefined 以构建动态查询?

  2. 它暴露的 API 是否足够底层,支持手写游标逻辑?

  3. 它的批量操作和 Upsert API 是否完整支持你所用数据库的特有功能(如 PG 的 JSONB 更新)?

  4. 它的 Migration 工具生成的 Diff 是否足够精准?

    到目前为止,我们已经构建了非常深厚的 “What” 和部分 “How” 的理论基础。接下来,你希望我们是继续探讨其他高级使用场景(比如事务处理、读写分离、JSONB 字段处理),还是正式进入关于你的知识库的下一环节:“为什么 (Why) 选择 / 不选择 ORM” 的深度对比与思考?