# 取形进阶

[「视图」](../getting-started/view)页讲了基本取形与嵌套 `.with`。这里的武器能让结果形状更贴近页面/接口的需要。

## 全量标量与剔除

```ts workspace
// ① 模型（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` 则把关联的字段**平铺到本级**（消除一层结构）：

```ts
// ② $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` 把分散字段/关联**折叠进一个新键**（重新分组，不改数据库结构）：

```ts
// ③ $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` 一次取整棵子树/祖先链：

```ts
// ④ $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 取原始值、结果壳上加工）：

```ts
// ⑤ mapOutput：价格脱敏/格式化
import { z } from 'zod'

const OutView = dto.view(Book, c => [
    c.id,
    c.name,
    c.price.mapOutput(z.string(), 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 序列化与 `?.` 解构习惯）：

```ts
const rows = await query.fetchList({ nullAsUndefined: true })
```

## 多态分支：$instanceOf

基类查询按子类型取分支字段（详见「继承与多态」两页）：

```ts
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 的完整实战。
