# 视图

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

先准备一组带关联的模型（模型页讲过：外键在 `Book`，`BookStore.books` 是它的反向集合）：

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

## 基本取形

声明一个视图，列出想要的字段：

```ts workspace
// ① 导入：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` 取数组**：

```ts
// 模型先备好双向关联（同一张外键的两种视角）：
//   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` 是数组。

### 引用键（reference key）：不展开对象，直接拿外键

m2o 的**拥有方**除了关联对象，还暴露**引用键**——用列名惯例驼峰直取外键标量（如 `STORE_ID` → `storeId`）：

```ts
// 只要外键值，不需要展开书店对象（与 c.store.with(c => [c.id]) 等价但更轻）
const BookStoreKeyView = dto.view(Book, c => [
    c.id,
    c.name,
    c.storeId,          // reference key：直取外键列（可空 m2o 时为 null）
])
```

embedded（复合键）外键同样有引用键 `c.orderId`，且能 `.with()` 继续取形/`$flat`（见「核心概念 → embedded → 多列外键」）。

## 关联可以不对称

`m2o` 与 `o2m` 是**同一张外键的两种视角**，但它们不需要成对出现：

- **外键永远在"多"的一侧**：本例外键列 `store_id` 在 `book` 表，所以 `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 是"怎么筛选、怎么排序"。下一节跑通完整的**查询**。
