定义 Fabric 应用的数据模型

Fabric应用使用 TypeScript 修饰器来定义生成数据库表和 API 的数据模型。 将每个实体定义为修饰的 @entity()类,为数据类型添加字段修饰器,以及映射实体之间的关系。

有关授权和访问控制指南,请参阅 “定义数据权限”。

先决条件

  • 使用 npm create @microsoft/rayfin@latest 创建或使用 npx rayfin init 初始化的 Fabric Apps 项目
  • 基本了解 TypeScript 类和修饰器。

定义实体

默认 blankapp 模板创建一个通用应用工作区,并禁用 Data。 通过模板的数据建模功能启用它:

npm run pack:add -- data-modeling

这一步会添加一个起始实体和共享契约。 把实体类放进packages/data/src/去,注册在 packages/data/src/index.ts,并在 packages/shared/src/index.ts 中维护面向客户端的契约。 数据服务在services.data.path: packages/data中使用rayfin/rayfin.yml。 当你手动配置Fabric的数据时,也要设置enabled: true和dialect: mssql。

平面模板可以将实体保留在 rayfin/data/ 中并使用 rayfin/data/schema.ts。 按照你配置好的服务路径走,而不是创建第二个数据文件夹。 参见 项目结构。

若要创建数据模型,请将 @entity() 修饰器添加到 TypeScript 类。 然后从 @microsoft/rayfin-core 导入所需的修饰器:

import { entity, uuid, text, date } from '@microsoft/rayfin-core';

@entity()
export class Todo {
  @uuid() id!: string;
  @text({ max: 200 }) title!: string;
  @text({ optional: true, max: 2000 }) description?: string;
  @date() createdAt!: Date;
  @date() updatedAt!: Date;
}

此实体生成一个 Todo 表,其中包含 id、title、description、createdAt 和 updatedAt 列。

Important

为持久化文本字段设定一个明确的最大长度,适合你的数据。 在 Microsoft SQL Server 上,不含 max 的文本会生成 NVARCHAR(MAX),这可能会阻止 GraphQL 元数据的生成。 示例中使用了有界文本,包括所有者字符串和外键字符串。 参见“排查数据模型问题”。

主键

每个实体都使用一个名为其主键的 string UUID id 字段。 如果未显式声明 id,Fabric Apps 会自动将其添加到架构中。

  • 在创建操作期间,该 id 字段是可选的, 如果省略该字段,服务器将生成 UUID。
  • 如果更喜欢客户端生成的标识符,可以在创建时提供自己的 UUID。
  • 不支持复合主键和自定义键名称。
@entity()
export class Note {
  @uuid() id!: string;  // UUID primary key, auto-generated when omitted
  @text({ max: 200 }) title!: string;
  @text({ max: 4000 }) content!: string;
}

支持的数据类型

使用这些修饰器定义字段类型:

装饰者 类型 说明
@uuid() 字符串 唯一标识符字段。
@text({ max: n }) 字符串 具有明确最大长度的持久化 MSSQL 列文本字段。
@int() number 整数字段。
@decimal() number 十进制或数字字段。
@boolean() 布尔 True 或 false 字段。
@date() 日期 日期和时间字段,从 ISO 字符串或 Date 对象序列化。
@email() 字符串 包含电子邮件验证的文本字段。
@set() 字符串 枚举的字符串文本集。

包含多种类型的示例

import { entity, uuid, text, int, decimal, boolean, date, set } from '@microsoft/rayfin-core';

@entity()
export class Product {
  @uuid() id!: string;
  @text({ max: 200 }) name!: string;
  @decimal() price!: number;
  @int() stockQuantity!: number;
  @boolean() isAvailable!: boolean;
  @date() createdAt!: Date;
  @set('draft', 'published', 'archived') status!: 'draft' | 'published' | 'archived';
}

类型修饰符

向字段修饰器添加修饰符以配置验证和约束:

修饰符 说明
{ optional: true } 允许 NULL 值。 字段默认为必填项。
{ unique: true } 添加唯一约束。
{ default: value } 设置默认值表达式。
{ max: n }、{ min: n } 字符串长度约束(最大字符数和最小字符数)。
{ min: n }、{ max: n } 数值约束。

注释

TypeScript 可选标记(? 属性名称之后)仅影响静态 TypeScript 类型。 它不会将数据库列设为可为 NULL。 若要将字段设为可为 null,请将 { optional: true } 添加到装饰器中。 用于 ! 断言所需字段由框架初始化。

带修饰符的示例

@entity()
export class User {
  @uuid() id!: string;
  @email({ unique: true, max: 254 }) email!: string;
  @text({ min: 3, max: 50 }) username!: string;
  @text({ optional: true, max: 500 }) bio?: string;
  @int({ min: 0, max: 150 }) age!: number;
  @boolean({ default: false }) isVerified!: boolean;
}

定义关系

使用 @one() 和 @many() 修饰器定义实体之间的导航属性。 Fabric应用在定义关系时自动生成外键列。

  • 一对多 – 在父项上使用 @many(),在子项上使用 @one()。
  • 多对一 – 在子级上使用 @one() 来引用父级。
  • 不支持多对多关系 - 请改用显式联接实体。

一对多关系的示例

import { entity, uuid, text, date, one, many } from '@microsoft/rayfin-core';

@entity()
export class Notebook {
  @uuid() id!: string;
  @text({ max: 200 }) name!: string;
  @date() createdAt!: Date;
  @many(() => Note) notes?: Note[];
}

@entity()
export class Note {
  @uuid() id!: string;
  @text({ max: 200 }) title!: string;
  @text({ max: 4000 }) content!: string;
  @date() createdAt!: Date;
  @text({ max: 36 }) notebook_id!: string;
  @one(() => Notebook) notebook?: Notebook;
}

在 @one(() => Notebook) 实体上定义 Note 时,Fabric Apps 会自动创建 notebook_id 外键列。 仅当计划在应用程序代码中读取或设置外键字段时,才显式声明外键字段。

外键命名约定

定义外键字段时,请使用 {property}_id 命名约定:

@entity()
export class Note {
  @uuid() id!: string;
  @text({ max: 36 }) notebook_id!: string;      // Foreign key field
  @one(() => Notebook) notebook?: Notebook;  // Navigation property
}

不支持自定义外键名称(foreignKey 和 targetKey 选项)。

参考系统实体

Fabric 应用不支持指向系统实体(例如内置的 @one() 实体)的 USER 关系。 要将一行关联到已登录用户,添加一个绑定的 user_id 文本字段,并从授权声明中填充它(通常为 claims.sub):

@entity()
export class Task {
  @uuid() id!: string;
  @text({ max: 200 }) title!: string;
  @text({ max: 255 }) user_id!: string;  // System user ID from claims.sub
}

在角色策略中按 user_id 筛选行,以实施按用户的访问控制。

在架构中注册实体

在通用应用工作空间中,在 packages/data/src/index.ts 中注册运行时实体类:

import { Note } from './Note.js';
import { Notebook } from './Notebook.js';

export const schema = [Note, Notebook];

保持 packages/shared/src/index.ts 中对应的记录接口和 UniversalAppSchema 映射与这些实体保持一致。 前端从@rayfin-app/shared导入UniversalAppSchema;它不导入经过修饰的运行时类。 如果你使用 Product 中的数值聚合示例,请将 添加到这两个注册项中。

对于 平面模板的替代方案,在 rayfin/data/schema.ts 中映射实体类型:

import type { Note } from './Note.js';
import type { Notebook } from './Notebook.js';

export type NotesAppSchema = {
  Note: Note;
  Notebook: Notebook;
};

每当你创建新实体时,更新你所选布局的注册和模式类型。

应用架构更改

定义或修改实体后,将更改应用到数据库中。 关于完整的部署和验证工作流程,请参见 “应用并验证模式变更”。

  1. 将更新的架构部署到Fabric:

    npx rayfin up db apply
    
  2. 如果架构更改包括破坏性操作(删除列、重命名表),CLI 会警告你,并拒绝继续。 使用 --force 跳过安全检查:

    npx rayfin up db apply --force
    

注释

使用 --force 可能会导致数据丢失。 在继续操作之前,请仔细查看列出的操作。

最佳做法

  • 仅当你需要在代码中读取或设置外键字段时,才定义这些字段——Fabric 应用会根据导航修饰器自动生成它们。
  • 在实体文件中使用带有 .js 扩展名的相对导入,以便生成的 ESM JavaScript 能够被正确解析。
  • 有关授权模式和基于角色的访问,请参阅 “定义数据权限”。

Troubleshooting

缺失的关系

如果 API 中未显示关系,请验证:

  • 导航修饰器(@one() 或 @many())存在。
  • 该实体已注册在你配置的数据包和共享架构中,或者在使用平面模板时注册在 rayfin/data/schema.ts 中。