DTO 哲学
如果你从传统 ORM(Prisma、TypeORM、Sequelize…)过来,ts-grm 最需要先理解的差异是:它没有实体对象。
传统 ORM:记录 → 实体对象
传统做法是把一行数据库记录实例化成实体对象(new Book(...)),字段是模型全集。带来的连锁问题:
- 对象总有全部字段——列表页只用 3 个字段,其余白白构造;
- 关联靠懒加载/预加载策略——
store用不用都由架构定,N+1 要靠经验避免; - 查询结果是一等对象,序列化/比较/变更检测都要额外机制。
ts-grm:记录 → 视图决定的形状(DTO)
反过来:查询者声明"我要什么形状",框架只物化那个形状。
// 一个模型,多种形状:列表页与详情页各自声明
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 的每个能力过一遍。