TSts-grm

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 的每个能力过一遍。