TSts-grm

视图

ts-grm 查询不返回实体对象,而是视图声明的精确 DTO 形状。视图就是"列一张想要的清单"。

先准备一组带关联的模型(模型页讲过:外键在 BookBookStore.books 是它的反向集合):

const BookStore = model("BookStore", "id", class {
    id = prop.i64()
    name = prop.str(100)
    books = prop.o2m(Book).mappedBy("store")
})

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()
})

基本取形

声明一个视图,列出想要的字段:

// ① 导入:model/prop 建模型,dto 建视图;sqlClient 是示例提供的客户端
import { model, prop, dto } from '@ts-grm/core'
import { sqlClient } from './infra/sql-client'

// ② 声明模型:Book 表四列(本示例先不带关联,聚焦视图本身)
const Book = model("Book", "id", class {
    id = prop.i64()
    name = prop.str(50)
    edition = prop.i32()
    price = prop.num(10, 2)
})

// ③ 声明视图:只要这三个字段 → 结果就只有这三个字段
const BookView = dto.view(Book, c => [
    c.id,
    c.name,
    c.price,
])

// ④ 查询:全表按视图取回
const rows = await sqlClient.createQuery(Book, (q, book) => {
    return q.select(book.fetch(BookView))
}).fetchList()

console.log(rows)
  • c.id / c.name 直接选标量列,未列出的字段不会出现;
  • 全选标量写 c.$allScalars,剔除个别字段用 .exclude("price")
  • 视图是类型也是运行时装订rows 的类型会推导为 { id, name, price }[],取错字段名编译期就报错。

嵌套取形:顺着关联取

关联两边都能按需深入——多对一用 .with 取对象,一对多用 .with 取数组

// 模型先备好双向关联(同一张外键的两种视角):
//   Book.store      m2o:拥有方,生成外键列 STORE_ID,外键可空(.nullable())
//   BookStore.books o2m:反向集合,mappedBy 指向 Book.store,不产生列

// m2o 方向:每本书带它的书店(store 是对象,无关联时为 null)
const BookWithStoreView = dto.view(Book, c => [
    c.$allScalars,
    c.store.with(c => [
        c.id,
        c.name,
    ]),
])

// o2m 方向:每个书店带它的书(books 是数组)
const StoreWithBooksView = dto.view(BookStore, c => [
    c.id,
    c.name,
    c.books.with(c => [
        c.id,
        c.name,
        c.edition,
    ]),
])

两种方向的 SQL 行为不同:store.with 一次 JOIN 或按主键批量加载;books.with 是多行展开成数组(「查询 API → 关联预取」细讲)。取形的结果里 store 是对象(或 null),books 是数组。

关联可以不对称

m2oo2m同一张外键的两种视角,但它们不需要成对出现:

  • 外键永远在"多"的一侧:本例外键列 store_idbook 表,所以 Book.store = prop.m2o(BookStore) 才是"拥有方"——.nullable() 决定外键可空(这本书可以没有书店);
  • 反向集合是可选的BookStore.books 只是"从书店看它的书"的便利视角,写不写都不影响外键与查询;不写它,从 Book 出发照样能带出 store
  • 需要时再补反向:prop.o2m(Book).mappedBy("store")"store" 必须是 Book 上真实存在的 m2o 属性名——mappedBy 名字写错会在模型解析时报错

这正是建表的直觉:外键建在哪张表,关联就在哪个模型上声明;反向只是视图层便利o2m 永远与某个 m2o(或 o2o)配对,没有独立存在——这也是它叫"反向"的原因。

为什么是"视图"而不是实体

传统 ORM 把记录实例化成实体对象,字段是模型全集;ts-grm 反过来——形状由查询方决定。带来的变化:

  • 网络传输与对象构造只做必要功,少字段少成本;
  • 同样的模型按场景取不同形状:列表页不要 price、详情页要全部;
  • 与保存侧呼应(Input DTO 按需声明"可写哪些字段"),一套模型两种方向。

下一步

视图是"想要的形状",查询 DSL 是"怎么筛选、怎么排序"。下一节跑通完整的查询