# 模型

模型描述一张表的形状。用 `model(name, idKey, class)` 声明，`class` 里的字段就是数据库列：

```ts workspace
// ① 导入：model/prop 建模型，dto 建视图；sqlClient 是示例提供的客户端
import { model, prop, dto } from '@ts-grm/core'
import { sqlClient } from './infra/sql-client'

// ② 声明书店模型（注意：此处先引用了后面才定义的 Book——模型解析是惰性的，顺序无关）
const BookStore = model("BookStore", "id", class {
    id = prop.i64()
    name = prop.str(100)
    books = prop.o2m(Book).mappedBy("store")   // 反向集合：纯逻辑，不产生列；“store”是 Book 上的真实属性名
})

// ③ 声明书模型：store 是“拥有方”——外键列 store_id 由它生成
const Book = model("Book", "id", class {
    id = prop.i64()
    name = prop.str(50)
    edition = prop.i32()
    price = prop.num(10, 2)
    store = prop.m2o(BookStore).nullable()   // 多对一：外键列按命名策略生成（本沙盒 → STORE_ID）；nullable = 可无书店
})

// ④ 查询：每本书带上它的书店（嵌套取形 c.store.with）
const rows = await sqlClient.createQuery(Book, (q, book) => {
    return q.select(book.fetch(
        dto.view(Book, c => [
            c.$allScalars,                          // 全部标量列
            c.store.with(c => [c.id, c.name]),      // 并取关联的书店(id, name)
        ])
    ))
}).fetchList()

console.log(rows)
```

要点：

- **第二个参数是主键属性名**（`"id"`）；复合主键用 `prop.embedded({...})`（「核心概念 → 复合值」）；
- **字段名即列名**：默认命名策略下自动映射为大写蛇形（`UPPER_SNAKE_CASE`：`name → NAME`、`store → STORE_ID`），可自定义策略（「SQL 后端 → 命名策略」）；
- **模型只是元数据**：`model(...)` 不实例化任何对象、不连数据库——查询结果的形状由视图决定；
- **关联是属性**：`m2o` 生成外键列（拥有方）；一对一 `o2o`、一对多 `o2m`、多对多 `m2m` 在「核心概念 → 关联」展开；
- **反向关联用 `mappedBy`**：`BookStore.books = prop.o2m(Book).mappedBy("store")` 是纯逻辑反向——不产生列、不建外键，`"store"` 是 `Book` 上真实外键属性的名字；
- **定义顺序无关**：模型解析是惰性的，`BookStore` 先引用后面才定义的 `Book` 完全合法（自引用用 `() => 模型` 形式，见「核心概念 → 关联」）。

## 属性类型

| 声明 | 语义 | PostgreSQL 对应 |
|---|---|---|
| `prop.i8()` / `i16()` / `i32()` / `i64()` | 有符号整数 | `SMALLINT` ~ `BIGINT` |
| `prop.num(p, s)` | 定点数 | `NUMERIC(p, s)` |
| `prop.f32()` / `f64()` | 浮点 | `FLOAT` / `DOUBLE PRECISION` |
| `prop.str(n)` | 定长字符串 | `VARCHAR(n)` |
| `prop.text()` | 长文本 | `TEXT` |
| `prop.bool()` | 布尔 | `BOOLEAN` |
| `prop.dt()` | 日期时间 | `TIMESTAMP` |
| `prop.json(schema)` | 带结构校验的 JSON | `JSON` / `JSONB`（`prop.jsonb`） |
| `prop.enum({...})` / `enumSet(...)` | 枚举 / 枚举集合 | 字符串列 |
| `prop.embedded({...})` | 复合结构（可作复合键） | 多列展开 |

两个容易踩的细节：

```ts
id = prop.i64()             // number
id = prop.i64().asString()  // string —— 超出 Number.MAX_SAFE_INTEGER 时用它
```

- 所有类型默认**非空**，加 `.nullable()` 允许 `NULL`；
- `i64().asString()` 是 ts-grm 的既定用法：读写时自动在 `string` 与数据库 `BIGINT` 间转换。

## 下一步

模型回答了"表里有什么"。**"这次查询拿什么回来"由视图回答**——下一节声明第一个视图。
