# DTO 哲学

如果你从传统 ORM（Prisma、TypeORM、Sequelize…）过来，ts-grm 最需要先理解的差异是：**它没有实体对象**。

## 传统 ORM：记录 → 实体对象

传统做法是把一行数据库记录实例化成**实体对象**（`new Book(...)`），字段是模型全集。带来的连锁问题：

- 对象总有全部字段——列表页只用 3 个字段，其余白白构造；
- 关联靠懒加载/预加载策略——`store` 用不用都由架构定，N+1 要靠经验避免；
- 查询结果是一等对象，序列化/比较/变更检测都要额外机制。

## ts-grm：记录 → 视图决定的形状（DTO）

反过来：**查询者声明"我要什么形状"，框架只物化那个形状**。

```ts
// 一个模型，多种形状：列表页与详情页各自声明
const BookItem = dto.view(Book, c => [c.id, c.name])          // 列表：两列
const BookDetail = dto.view(Book, c => [c.$allScalars,        // 详情：全列 + 关联
    c.store.with(c => [c.id, c.name]),
])

// 类型即形状：rows 的类型就是 { id, name }[]，绝不夹带别的字段
const rows = await sqlClient.createQuery(Book, (q, book) => {
    return q.select(book.fetch(BookItem))
}).fetchList()
```

- 返回的是**纯数据对象**（DTO），没有隐藏的实体状态：可以序列化、可以直接进响应；
- 视图的类型 = 运行时形状，编译期锁定——**取错字段名直接编译失败**；
- 关联取不取、取几层，都由查询方按页面的真实需要决定，不需要框架猜。

## 两个方向都取形

视图决定"读什么形状"；**Input DTO** 决定"写什么形状"（「变更 API」章）——保存侧的字段白名单、key 语义、级联方式同样由形状声明驱动。一套模型，读的形状和写的形状都按需声明，这正是"像 GraphQL，而且是双向的"。

## 代价：类型体操

"形状由查询方决定 + 编译期类型推导"意味着大量类型级编程：`dto.view` 的返回类型不是手写的 interface，而是由映射推导出来的。这带来两个现实：

- **声明式约束**：写错属性名、取错 null 处理，编译期就报错——运行期更少意外；
- **学习曲线**：类型报错信息有时很深（内部类型会出现在推导链路里），这是换取"零反射 + 精确形状"的代价。

## 一句话总结

> ts-grm 里，**模型描述可能性（表有什么），视图/Input 描述需求（这次要什么）**；框架在两者之间生成最直接的 SQL 与精确的 DTO，全程无实体对象、无运行时反射。

## 下一步

核心概念全部就位：模型、属性、关联、继承、复合值、公式、DTO 哲学。接下来是功能大头——**查询 API**，把 DSL 的每个能力过一遍。
