# 查询

模型有了、视图有了，把它们接起来就是查询。`createQuery(model, (q, table) => …)` 以回调形式构建查询：

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

// ② 声明模型：Book 表（id/name/edition/price 四列）；字段即列，类型即约束
const Book = model("Book", "id", class {
    id = prop.i64()          // 主键，64 位整数
    name = prop.str(50)      // 定长字符串(50)
    edition = prop.i32()     // 32 位整数
    price = prop.num(10, 2)  // 定点数：总长 10、小数 2 位（金额）
})

// ③ 声明视图：本次查询只取 id/name/price 三列——查询结果就是这个形状
const BookView = dto.view(Book, c => [
    c.id,
    c.name,
    c.price,
])

// ④ 构建并执行查询：价格 > 40 的书，按价格从高到低
const rows = await sqlClient.createQuery(Book, (q, book) => {
    q.where(book.price.gt(40))            // 过滤：price > 40
    q.orderBy(book.price.desc())          // 排序：价格降序
    return q.select(book.fetch(BookView)) // 取形：按 BookView 返回
}).fetchList()                            // 收尾：执行，返回数组

console.log(rows)  // 结果面板会输出数组
```

运行结果（沙盒种子数据）：

```json
[
  { "id": 2, "name": "SQL Mastery", "price": 79 },
  { "id": 1, "name": "TypeScript in Depth", "price": 59.9 },
  { "id": 3, "name": "ORM Patterns", "price": 45.5 }
]
```

调用形态拆解：

| 一段 | 作用 |
|---|---|
| `createQuery(Book, (q, book) => …)` | 从 `Book` 发起查询；`book` 是**表对象**，属性即列表达式 |
| `q.where(book.price.gt(40))` | 过滤条件；多个 `where` 之间默认 AND |
| `q.orderBy(book.price.desc())` | 排序；可写多列（`asc` 为默认） |
| `q.select(book.fetch(BookView))` | 按视图取形 |
| `.fetchList()` | 收尾执行，返回视图类型数组 |

注意与常见 ORM 的差异：**条件写在列表达式上**（`book.price.gt(40)`），而不是 `where({ price: { gt: 40 } })` 对象字面量——写错属性名编译期就报错。

## 条件表达式

```ts
book.id.eq(3)               // 等于
book.price.ne(45.5)         // 不等于
book.price.gt(50).lt(80)    // 大于 / 小于（可链式）
book.price.between(30, 80)  // 区间
book.name.ilike("sql")      // 不区分大小写包含匹配（like 区分大小写）
book.id.in(1, 2, 3)         // IN
```

动态条件用 `xxxIf` 系列：`book.price.gteIf(search.min)`——参数为 `null`/`undefined` 时该条件自动省略（「查询 API → 条件表达式」展开）。

## 收尾方法

查询对象（`createQuery` 的返回值）按需求选一个收尾方法执行：

| 方法 | 语义 | 返回 |
|---|---|---|
| `fetchList()` | 多行 | `T[]` |
| `fetchRequired()` | **恰好一行**：0 行抛 `NoDataError`、多行抛 `TooManyDataError` | `T` |
| `fetchOptional()` | **至多一行**：无行返回 `null`（`{ nullAsUndefined: true }` 时返回 `undefined`） | `T \| null` |
| `fetchPage({ pageNo?, pageSize })` | 分页：总数 + 当前页（见下） | `Page<T>` |
| `fetchRange({ limit, offset? })` | 区间：跳过 offset 行取 limit 行 | `T[]` |
| `fetchCount()` | 行数 | `number` |

**没有 `.fetchOne()`**——"必须有且仅有一行"用 `fetchRequired()`，"至多一行"用 `fetchOptional()`，不要把行数例外留给自己处理。

分页示例——一次拿回总数与分页信息：

```ts
// fetchPage：按页取数。pageNo 从 1 开始，缺省 1；pageSize 必填
// （片段：完整可运行版见工作区）
const page = await sqlClient.createQuery(Book, (q, book) => {
    q.orderBy(book.id)                     // 分页必须配合稳定排序，否则页间数据会漂移
    return q.select(book.fetch(BookView))
}).fetchPage({ pageNo: 1, pageSize: 2 })

console.log(page.totalRowCount)  // 3（种子数据共 3 本）
console.log(page.pageNo)         // 1
console.log(page.isFirstPage)    // true
console.log(page.isLastPage)     // false
console.log(page.rows)           // 第 1 页的 2 行
```

`Page<T>` 结构：`totalRowCount / totalPageCount / pageNo / isFirstPage / isLastPage / rows`。

## 下一步

「安装 → 模型 → 视图 → 查询」闭环已完成，你已经能在沙盒里跑 ts-grm 了（环境与数据库抽屉的使用见[「沙盒环境」](../about/sandbox)页）。接下来进入**核心概念**：模型的另一半——关联、继承与多态、复合值，以及整个框架的理念基石[「DTO 哲学」](../core-concepts/dto-philosophy)。
