# 公式与计算属性

**公式（Formula）** 让计算值表现得像普通属性：能放进视图、参与取形与排序。区别只在"值从哪来"。

## SqlFormula：在 SQL 层计算

写在 SQL 里的表达式或子查询。示例数据中已有 `author` 表与 `book_author_mapping`，下面这个例子可以直接运行：

```ts workspace
// ① 导入：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 里计算——适合拼接、加工、聚合后处理：

```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** 算"一组行的目标集合"（如"每本书店的最新书"）：

```ts
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 哲学**：为什么整个框架围绕"形状"而不是"实体"运转。
