TSts-grm

关联预取与批加载

视图里一行 .with,框架自动决定怎么取关联数据。理解背后的两种 SQL 策略,才能写好高性能查询。

m2o:双语句批加载

嵌套 c.store.with(...)(n+1 场景)默认不 join,而是两条语句:

// ① 模型(完整可运行版见工作区)
import { model, prop, dto } from '@ts-grm/core'
import { sqlClient } from './infra/sql-client'

const BookStore = model("BookStore", "id", class {
    id = prop.i64()
    name = prop.str(100)
})

const Book = model("Book", "id", class {
    id = prop.i64()
    name = prop.str(50)
    edition = prop.i32()
    price = prop.num(10, 2)
    store = prop.m2o(BookStore).nullable()
})

const BookView = dto.view(Book, c => [
    c.$allScalars,
    c.store.with(c => [c.id, c.name]),
])

const rows = await sqlClient.createQuery(Book, (q, book) => {
    return q.select(book.fetch(BookView))
}).fetchList()

console.log(rows.map(r => `${r.name} @ ${r.store?.name}`).join('\n'))

跑完看结果面板的 SQL 追踪,是两段:

select ... from BOOK tb_1_                              -- 主查询
select ID, NAME from BOOK_STORE tb_1_ where ID in(?, ?) -- 加载关联:按主查询收集的去重 id 批量取
  • 第二段的 IN 列表 = 主查询结果里收集的去重 store_id——恒为"两段",不随结果行数膨胀成 N 条(天然防 N+1);
  • 关联条数不多时,这种方式比 JOIN 更省(避免行数膨胀与列重复)。

o2m / m2m:key 匹配后展开

集合方向(booksauthors)同理:先查主表,再按主表主键 IN 把关联集合按 source 分组回填成数组。SQL 追踪同样能看到第二段形如 select ... from BOOK where STORE_ID in(?, ?)

何时会 join:fetchOptions

偏移/分页很大、或需要按关联列排序/过滤时,批加载不一定会退化成 JOIN。相关控制:

选项作用
maxJoinFetchDepthjoin 取形的最大嵌套深度(默认 5,超限报错而非静默折衷)
maxJoinFetchOffset关联批加载退化为 join 的偏移阈值
defaultBatchSize / defaultListBatchSize主/集合方向的批加载 IN 上限(默认 128 / 16)

它们都是 newSqlClient(driver, options) 的构建选项(「SQL 后端」章展开)。

关联顺序

集合方向可声明默认排序(prop.o2m(...).orderBy(...)),取形时自动带上;查询内也能 q.orderBy(book.store.name) 之类的关联列排序(会触发对应取形策略)。

内功心法

  • 恒两段、按 key 批加载是 ts-grm 的默认取形路线——界面列多、行多时最省;
  • 需要 join 语义(关联列过滤/排序、行数精确)时用条件/排序驱动,或者干脆把关联列展开成普通列查询;
  • 复杂批加载与 join 的实测量化,可在你自己的库里开 SQL 追踪对比(见「SQL 日志与追踪」)。

本章小结

查询 API 十页收束:根查询 → 条件 → 排序 → 取形 → 分页 → 聚合 → 子查询 → CTE → 原生 SQL → 关联预取。下一章进入 SQL 后端:驱动、事务、方言与命名策略。