TSts-grm

复合值 embedded

当一组列总是作为一个整体出现(姓名、地址、坐标),可以用 prop.embedded 把它们打包成一个值对象:

// ① 作者:name 是复合值(两列打包)
const AUTHOR = model("Author", "id", class {
    id = prop.i64()
    name = prop.embedded({
        firstName: prop.str(50),
        lastName: prop.str(50),
    })
})

表结构上仍然是普通列,而且成员名直接作列名(不拼接父属性路径):name.firstName → FIRST_NAME、name.lastName → LAST_NAME。模型侧是一个结构,查询、取形、排序都按路径访问。

同名成员会静默共用一列:正因为列名不带前缀,同一模型里若两个 embedded 含同名成员(如 audit.deletedAt 与 sync.deletedAt),或 embedded 成员与普通字段重名,最终只会生成一列、读写同一存储——而且不会报错(a.v 与 b.v 都落在 V 上)。复用通用字段时记得用限定名(syncDeletedAt)或错开结构来区分。

复合主键

主键同样可以嵌入多列,甚至嵌套:

const ORDER = model("Order", "id", class {
    // id 是一个复合结构:x 与嵌套的 y{a,b}——共三列做主键(列名 X / A / B)
    id = prop.embedded({
        x: prop.i32(),
        y: prop.embedded({
            a: prop.i16(),
            b: prop.i16(),
        }),
    })
    name = prop.str(10)
})

路径引用

复合值内部通过子路径访问,写法和属性访问一致:

// ② 复合值的路径:取名、按姓排序(片段:需在查询上下文中使用)
q.where(author.name().firstName.eq("Tao"))      // WHERE FIRST_NAME = ?
q.orderBy(author.name().lastName)               // ORDER BY LAST_NAME

// 复合主键的子路径:排序、查询都可以(下例取 Tag 的复合 id —— 注意 `id()` 是函数,
// 每一层 embedded 都是函数调用,返回作用域后再取标量子路径)
q.orderBy(tag.id().low, tag.id().high)            // 复合 id 的 low/high 分量

多列外键

关联指向复合主键时,外键就是一组列。joinColumns 的 referencedSubPath 把本侧列映射到对方复合键的分量:

const ORDER_ITEM = model("OrderItem", "id", class {
    id = prop.i64()
    order = prop.m2o(ORDER).joinColumns({
        columns: [
            { columnName: "order_x", referencedSubPath: "x" },
            { columnName: "order_y_a", referencedSubPath: "y.a" },
            { columnName: "order_y_b", referencedSubPath: "y.b" },
        ],
        cascade: "DELETE",
    })
})

referencedSubPath 用点号指到对方复合键的具体分量;m2m 中间表(joinThis/referenceTarget 的 columns)同理支持复合键。

引用键取形:embedded 也能 .with / $flat

多列外键在取形时可以先取引用键(c.orderId,embedded 形状),既能原样收下,也能继续取形/扁平:

// ④ 引用键原样:orderId 是一个 embedded 对象({ x, y: { a, b } })
const OrderKeyView = dto.view(OrderItem, c => [
    c.id,
    c.orderId,
])

// ⑤ 引用键取形 + embedded 内部 flat:只要需要的一部分,且摊平
const OrderKeyFlatView = dto.view(OrderItem, c => [
    c.id,
    c.orderId.with(c => [
        c.x,
        c.$flat("y").prefix("").with(c => [c.b]),   // y.b 摊到 orderId 本级(可 prefix("") 去前缀)
    ]),
])

普通 embedded 属性也支持 c.$flat("name")(见「查询 → 取形进阶 → $flat 扁平化」)。

定位:复用字段用 embedded,不要用继承

单纯想把一组列打包复用(姓名、地址、坐标)时,用 embedded 而不是继承——继承会引入单表继承与判别列(见「继承与多态」),为了省重复代码而继承得不偿失。embedded 只是结构打包,列仍是普通列,不会改变表形态。

下一步

列、关联、形态都齐了,模型还有一个经常被高估复杂度的能力:公式与计算属性——把 SQL 表达式或 TS 计算挂到模型的普通属性上。