TSts-grm

模型

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

// ① 导入: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_CASEname → NAMEstore → STORE_ID),可自定义策略(「SQL 后端 → 命名策略」);
  • 模型只是元数据model(...) 不实例化任何对象、不连数据库——查询结果的形状由视图决定;
  • 关联是属性m2o 生成外键列(拥有方);一对一 o2o、一对多 o2m、多对多 m2m 在「核心概念 → 关联」展开;
  • 反向关联用 mappedByBookStore.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)带结构校验的 JSONJSON / JSONBprop.jsonb
prop.enum({...}) / enumSet(...)枚举 / 枚举集合字符串列
prop.embedded({...})复合结构(可作复合键)多列展开

两个容易踩的细节:

id = prop.i64()             // number
id = prop.i64().asString()  // string —— 超出 Number.MAX_SAFE_INTEGER 时用它
  • 所有类型默认非空,加 .nullable() 允许 NULL
  • i64().asString() 是 ts-grm 的既定用法:读写时自动在 string 与数据库 BIGINT 间转换。

下一步

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