公式与计算属性
公式(Formula) 让计算值表现得像普通属性:能放进视图、参与取形与排序。区别只在"值从哪来"。
SqlFormula:在 SQL 层计算
写在 SQL 里的表达式或子查询。示例数据中已有 author 表与 book_author_mapping,下面这个例子可以直接运行:
// ① 导入:model/prop/dsl/dto 建模型与公式,z 声明值类型;sqlClient 是示例提供的客户端
import { model, prop, dsl, dto, SqlFormula } from '@ts-grm/core'
import { z } from 'zod'
import { sqlClient } from './infra/sql-client'
// ② 作者模型:books 是 m2m 反向(映射表 book_author_mapping)
const Author = model("Author", "id", class {
id = prop.i64()
name = prop.embedded({
firstName: prop.str(50),
lastName: prop.str(50),
})
books = prop.m2m(Book).mappedBy("authors")
})
// ③ 书模型:authors 是 m2m 拥有侧(中间表配置与示例数据一致)
const Book = model("Book", "id", class {
id = prop.i64()
name = prop.str(50)
edition = prop.i32()
price = prop.num(10, 2)
authors = prop.m2m(Author).joinTable({
name: "book_author_mapping",
joinThisColumns: ["book_id"],
joinTargetColumns: ["author_id"],
})
// 公式:作者数量(SQL 子查询 count)——声明后像普通属性一样用
authorCount = prop.formula.sql(BOOK_AUTHOR_COUNT_FORMULA)
})
// ④ 定义 SQL 公式:对每本书,数它关联表里的作者行
const BOOK_AUTHOR_COUNT_FORMULA: SqlFormula<number> =
SqlFormula.of({
valueType: z.number(), // StandardSchemaV1:类型来源
sourceModel: () => Book, // 挂在哪个模型上
fn: book => dsl.subQuery(
dsl.associationModel(Book, "authors"), // 走 Book.authors 关联
(q, assoc) => {
q.where(assoc.sourceId.eq(book.id)) // 关联到当前行
return q.select(dsl.count())
}
),
})
// ⑤ 查询:公式列出现在结果里(authorCount 由 SQL 计算)
const rows = await sqlClient.createQuery(Book, (q, book) => {
return q.select(book.fetch(dto.view(Book, c => [
c.id,
c.name,
c.authorCount,
])))
}).fetchList()
console.log(rows) // 每本书带 authorCount(1~2)
注意常量和模型的声明顺序:公式定义引用了 Book,但 Book 也在公式定义之后才完整——和模型一样,顺序无关(惰性解析)。
TsFormula:在内存计算
读入依赖的数据后在 TS 里计算——适合拼接、加工、聚合后处理:
// 书店的书名清单:依赖 books 集合,fn 纯函数加工
const BOOK_NAMES_FORMULA: TsFormula<ReadonlyArray<string>> =
TsFormula.of({
valueType: z.array(z.string()),
dependency: () => dto.view(BookStore, c => [ // 声明要读哪些数据
c.books.with(c => [c.name, c.edition]),
]),
fn: data => data.books.map(b => `${b.name}(${b.edition})`), // 内存计算
})
const BookStore = model("BookStore", "id", class {
id = prop.i64()
name = prop.str(100)
bookNames = prop.formula.ts(BOOK_NAMES_FORMULA)
})
Calculator:集合型计算属性
公式算"单行一个值";Calculator 算"一组行的目标集合"(如"每本书店的最新书"):
const NEWEST_BOOKS = Calculator.targetOf({
sourceModel: () => BookStore, // 从哪个模型出发
targetModel: () => Book, // 算出什么模型
fn: ctx => ctx.sqlClient.createQuery(Book, (q, book) => {
// ctx.keys 是当前批次的所有书店 id——自动批量化,避免 N+1
q.where(book.storeId.in(...ctx.keys))
...
}).fetchList(),
})
newestBooks = prop.calculated.collection(NEWEST_BOOKS) // 集合属性
另有带参数的 Calculator.parameterizedTargetOf(参数经 zod 校验)。
何时用哪个
- 能写进 SQL 的(count、表达式) →
SqlFormula:还能排序/过滤; - 必须 JS 加工的 →
TsFormula:声明依赖,框架自动补读; - 跨行的聚合到目标集合 →
Calculator:天然批量化。
下一步
模型体系至此完整:列、关联、继承、复合值、公式。下一节是最重要的一页——DTO 哲学:为什么整个框架围绕"形状"而不是"实体"运转。