在 Fabric Apps 中使用 GraphQL 读取和写入数据

Fabric Apps 提供了一个类型安全的 GraphQL 客户端,使你无需编写原始查询即可执行创建、读取、更新和删除操作。 客户端会根据您的方法调用自动生成 GraphQL,并基于您的数据模型定义返回类型化实体。

先决条件

  • 定义数据模型的Fabric应用项目。 请参阅 “定义数据模型”。
  • 在本地运行或部署到 Fabric 的后端服务。

初始化客户端

使用您的后端 URL、可公开密钥和 schema 类型来实例化 RayfinClient:

import { RayfinClient } from '@microsoft/rayfin-client';
import type { UniversalAppSchema } from '@rayfin-app/shared';

const client = new RayfinClient<UniversalAppSchema>({
  baseUrl: import.meta.env.VITE_RAYFIN_API_URL,
  publishableKey: import.meta.env.VITE_RAYFIN_PUBLISHABLE_KEY,
});

泛型类型参数使 TypeScript 能够为所有数据操作提供自动完成和类型检查。

这些例子假设你启用了 Data,并在通用应用的 packages/data/src/index.ts 和共享架构协定中注册了 Note 和 Notebook。 在平面模板中,请改为从 rayfin/data/schema.ts 导入你的模式类型。 请参阅 “定义数据模型”。

读取数据

通过 client.data.<EntityName> 访问实体集合。 Fluent API 提供了用于查询、筛选、排序和分页的方法。

获取一页记录

execute() 默认只返回一页,包含100条记录。 它不会返回光标元数据,也不会获取后续页的结果。 对于可能超过一页的列表,使用前向分页。

const notes = await client.data.Note.select([
  'id',
  'title',
  'content',
  'createdAt',
  'isPinned',
]).execute();

按主键提取单个记录

const note = await client.data.Note.findById('00000000-0000-0000-0000-000000000000');

findById() 如果没有记录,则返回 null 。 发布的实现只选择主键,所以不要假设其他实体字段都被填充。 当你需要它们的值时,明确选择字段:

const notes = await client.data.Note.select(['id', 'title', 'content'])
  .where({ id: { eq: '00000000-0000-0000-0000-000000000000' } })
  .first(1)
  .execute();

const note = notes[0] ?? null;

筛选记录

使用where()方法筛选结果:

const pinnedNotes = await client.data.Note.select([
  'id',
  'title',
  'isPinned',
])
  .where({ isPinned: { eq: true } })
  .execute();

过滤器操作符

Operator 说明 例子
eq 等于 { status: { eq: 'active' } }
neq 不等于 { status: { neq: 'archived' } }
gt 大于 { age: { gt: 18 } }
gte 大于或等于 { age: { gte: 21 } }
lt 小于 { price: { lt: 100 } }
lte 小于或等于 { price: { lte: 50 } }
contains 包含子字符串 { title: { contains: 'draft' } }

对结果进行排序

用于 orderBy() 对查询结果进行排序:

const notes = await client.data.Note.select([
  'id',
  'title',
  'createdAt',
])
  .orderBy({ createdAt: 'desc' })
  .execute();

按多个列排序:

const notes = await client.data.Note.select([
  'id',
  'title',
  'isPinned',
  'createdAt',
])
  .orderBy({ isPinned: 'desc' })
  .orderBy({ createdAt: 'desc' })
  .execute();

定义与@one()@many()修饰器的关系时,可以在同一查询中包含相关实体字段:

const notes = await client.data.Note.select([
  'id',
  'title',
  'content',
  'notebook.id',
  'notebook.name',
])
  .execute();

每个注释都包含其关联的笔记本数据,而无需单独的查询。

对较大的结果集进行分页

对大型列表使用基于游标的分页:

const page = await client.data.Note.select([
  'id',
  'title',
  'createdAt',
])
  .orderBy({ createdAt: 'desc' })
  .orderBy({ id: 'asc' })
  .first(25)
  .executePaginated();

console.log('Items:', page.items);
console.log('Has next page:', page.hasNextPage);
console.log('End cursor:', page.endCursor);

使用光标提取下一页:

if (page.hasNextPage) {
  if (!page.endCursor) {
    throw new Error('The next page is missing its cursor.');
  }
  const nextPage = await client.data.Note.select([
    'id',
    'title',
    'createdAt',
  ])
    .orderBy({ createdAt: 'desc' })
    .orderBy({ id: 'asc' })
    .first(25)
    .after(page.endCursor)
    .executePaginated();
}

注释

后端不会在totalCount类型上填充PagedResult属性。 该 page.items.length 值只计算当前页面,而非所有匹配记录。

要处理所有匹配记录,向前推进直到 hasNextPage 为 false。 保持选择、筛选和排序顺序在不同请求中保持不变。

let cursor: string | undefined;
let matchingRecords = 0;

while (true) {
  let query = client.data.Note.select(['id'])
    .where({ isArchived: { eq: false } })
    .orderBy({ id: 'asc' })
    .first(100);

  if (cursor !== undefined) {
    query = query.after(cursor);
  }

  const page = await query.executePaginated();
  matchingRecords += page.items.length;

  if (!page.hasNextPage) {
    break;
  }
  if (!page.endCursor) {
    throw new Error('The next page is missing its cursor.');
  }
  cursor = page.endCursor;
}

console.log('Records read across all pages:', matchingRecords);

分页不能提供跨请求的快照。 同时发生的变化会影响您读取和统计的记录。

汇总和分组数据

使用 aggregate() 进行服务器端总计,并使用 groupBy() 进行每组汇总。 独立的 fluent 方法 count() 不可用,但在聚合规范中支持 count。

所有五个聚合运算(sum、avg、min、max 和 count)都需要一个数值字段。 不要把UUID id 或文本字段算进去。 例如,以下查询假设一个注册 Product 实体,具有要求的数值 stockQuantity 字段:

const totals = await client.data.Product.where({ isAvailable: { eq: true } })
  .aggregate({
    matchingProducts: { count: 'stockQuantity' },
    totalStock: { sum: 'stockQuantity' },
    averageStock: { avg: 'stockQuantity' },
    lowestStock: { min: 'stockQuantity' },
    highestStock: { max: 'stockQuantity' },
  })
  .execute();

console.log(totals[0].aggregations.matchingProducts);

count 计数指定数值字段的非空值。 使用必填的数值字段来计数匹配的行数。 如果您的实体没有合适的字段,请如前所述统计 所有 页面的标识符,而不仅仅是单个页面。

对于分组总计:

const groups = await client.data.Product.groupBy(['status'])
  .aggregate({
    matchingProducts: { count: 'stockQuantity' },
    totalStock: { sum: 'stockQuantity' },
  })
  .execute();

for (const row of groups) {
  console.log(row.fields.status, row.aggregations.totalStock);
}

聚合结果是包含 fields 和 aggregations 对象的数组。 总计中有一行是空 fields 对象。 count 返回 number;其他操作则对于空输入或全空输入返回 number | null。

在聚合前应用 where()。 不要将聚合与行选择、排序或分页结合,或通过 select()、orderBy()、first() 或 after()。

创建记录

create()使用该方法插入新记录:

const newNote = await client.data.Note.create({
  title: 'Meeting notes',
  content: 'Discussion points from the team sync',
  isPinned: false,
  isArchived: false,
  createdAt: new Date(),
  updatedAt: new Date(),
  user_id: 'user-123',
});

该方法返回已创建的记录,包括其自动生成的 id。 当你需要额外的存储字段时,请使用显式 selected read。

创建具有关联关系的记录

创建具有关联关系的实体时,传入完整的关联对象,或仅传入包含主键的对象:

// Option 1: Pass just the ID
const note = await client.data.Note.create({
  title: 'Weekly summary',
  content: 'Summary of this week',
  notebook: { id: '22222222-3333-4444-5555-666666666666' },
  isPinned: false,
  isArchived: false,
  createdAt: new Date(),
  updatedAt: new Date(),
});

// Option 2: Select the related fields you need before creating the note.
const notebooks = await client.data.Notebook.select(['id', 'name'])
  .where({ id: { eq: '22222222-3333-4444-5555-666666666666' } })
  .first(1)
  .execute();
const notebook = notebooks[0];
if (!notebook) {
  throw new Error('Notebook not found.');
}

const noteWithNotebook = await client.data.Note.create({
  title: 'Weekly summary',
  content: 'Summary of this week',
  notebook,
  isPinned: false,
  isArchived: false,
  createdAt: new Date(),
  updatedAt: new Date(),
});

这两种形式生成相同的结果。 当你已经知道相关实体的 ID,且想避免额外的获取时,请使用第一种形式。

客户端使用相关对象的主键来设置关系。 传递一个对象并不会创建或更新相关的笔记本。

更新记录

使用 update() 方法修改现有记录。 传递筛选器对象和包含要更新的字段的对象:

await client.data.Note.update(
  { id: 'note-123' },
  {
    title: 'Updated title',
    updatedAt: new Date(),
  }
);

更新关系

若要更改关系,请传递新的相关实体或其 ID:

// Move a note to a different notebook
await client.data.Note.update(
  { id: 'note-123' },
  { notebook: { id: 'new-notebook-789' } }
);

删除记录

使用此方法 delete() 删除与筛选器匹配的记录:

await client.data.Note.delete({ id: 'note-123' });

该方法会在后端确认已删除后完成。 如果没有与筛选器匹配的记录,该方法仍然成功。

创建或更新记录

使用 upsert(where, create, update) 创建缺失记录或更新现有记录。 查找时和创建输入中提供相同的ID:

const id = '11111111-2222-3333-4444-555555555555';
const now = new Date();

await client.data.Note.upsert(
  { id },
  {
    id,
    title: 'Daily summary',
    content: 'Summary of today',
    isPinned: false,
    isArchived: false,
    createdAt: now,
    updatedAt: now,
    user_id: 'user-123',
  },
  { title: 'Daily summary', updatedAt: now },
);

该助手先进行读取,然后单独创建或更新。 这不是原子级的,并发调用可能会发生竞争。 不要把它当作事务或并发保证。

处理身份验证

在执行受保护数据操作前请登录。 对于已部署的后端,可以使用应用配置的 Microsoft Entra 登录流程。 以下电子邮件地址和密码示例仅适用于本地后端:

await client.auth.signIn({ email, password });

// All subsequent data calls include authentication context
const notes = await client.data.Note.select(['id', 'title']).execute();

客户端会自动将身份验证会话附加到所有数据 API 调用。 无需手动传递令牌。

最佳做法

  • 仅选择所需的字段 – 仅提取用于减小有效负载大小并提高性能的字段。
  • 对大型列表使用分页 – 避免使用 first() 和 executePaginated()一次性提取数千条记录。
  • 批处理关系查询 - 在同一查询中包含相关实体字段,而不是发出单独的请求。
  • 缓存经常访问的数据 - 在内存中存储静态引用数据以减少 API 调用。

当前限制

  • 目前没有可单独使用的 count() 方法。 使用数值字段 聚合,或者在所有页面上计数记录。 数组的长度仅计算该页面返回的记录。
  • 不支持多对多关系。 使用一个显式联结实体,并配合两个 @one() 导航修饰器。
  • totalCount 上的 PagedResult 属性不是由后端填充的。