取形进阶
「视图」页讲了基本取形与嵌套 .with。这里的武器能让结果形状更贴近页面/接口的需要。
全量标量与剔除
// ① 模型(book + store,完整可运行版见工作区)
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()
})
// $allScalars:全部标量列;.exclude 剔除个别
const BookView = dto.view(Book, c => [
c.$allScalars.exclude("price"),
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[0])
$flat 扁平化:把关联「摊平」
.with 取嵌套对象;$flat 则把关联的字段平铺到本级(消除一层结构):
// ② $flat:store 的字段平铺成 storeName / storeId(前缀可自定)
const FlatView = dto.view(Book, c => [
c.id,
c.name,
c.$flat("store").with(c => [
c.id,
c.name,
]),
])
const flatRows = await sqlClient.createQuery(Book, (q, book) => {
return q.select(book.fetch(FlatView))
}).fetchList()
console.log(flatRows[0]) // { id, name, storeId, storeName, ... }
- 结果形状:
store消失,字段带前缀(默认store前缀,即storeId/storeName); - 自定义前缀:
c.$flat("store").prefix("shop").with(...)→shopId/shopName;prefix("")则无前缀直接并排; - embedded 也可以扁平:
c.$flat("name").with(...)把name.firstName摊成firstName。
$fold 折叠:把多个来源合成一组
$fold 把分散字段/关联折叠进一个新键(重新分组,不改数据库结构):
// ③ $fold:书的关键信息折叠成一个 info 对象
const FoldView = dto.view(Book, c => [
c.id,
c.$fold("info", c => [
c.name,
c.edition,
c.$flat("store").with(c => [c.name]), // 嵌套扁平也可以
]),
])
const foldRows = await sqlClient.createQuery(Book, (q, book) => {
return q.select(book.fetch(FoldView))
}).fetchList()
console.log(foldRows[0]) // { id, info: { name, edition, storeName } }
$recursive 递归:树形结构一层取到底
自关联模型(如 TreeNode.parentNode / childNodes)用 $recursive 一次取整棵子树/祖先链:
// ④ $recursive:每条树节点连同其全部后代(子树)
const NodeView = dto.view(TreeNode, c => [
c.id,
c.name,
c.$recursive("childNodes").with(c => [
c.id,
c.name,
c.$recursive("childNodes").with(c => [c.id]),
]),
])
.as(别名)可改名(如$recursive("parentNode").as("upObj"));- 深度控制与防环选项在视图配置中调整(自关联必须声明合法,否则模型解析报错)。
mapOutput:输出前转换
视图字段可以声明输出映射(运行时转换语义,SQL 取原始值、结果壳上加工):
// ⑤ mapOutput:价格脱敏/格式化
import { z } from 'zod'
const OutView = dto.view(Book, c => [
c.id,
c.name,
c.price.mapOutput(z.number(), v => `¥${v.toFixed(2)}`),
])
const outRows = await sqlClient.createQuery(Book, (q, book) => {
return q.select(book.fetch(OutView))
}).fetchList()
console.log(outRows[0]) // { id, name, price: '¥59.90' }
nullAsUndefined:空值处理
fetchOptional 与 fetchList 支持 { nullAsUndefined: true }:可空字段/无行结果从 null 变成 undefined(贴合 JSON 序列化与 ?. 解构习惯):
const rows = await query.fetchList({ nullAsUndefined: true })
多态分支:$instanceOf
基类查询按子类型取分支字段(详见「继承与多态」两页):
const StoreView = dto.view(BookStore, c => [
c.id, c.name,
c.$instanceOf(PhysicalBookStore, c => [c.city, c.street]),
c.$instanceOf(OnlineBookStore, c => [c.url]),
])
下一步
查询的形状武器齐了。下一节分页与区间:fetchPage / fetchRange / fetchCount 的完整实战。