读取与查询
生成的客户端以两种方式读取聚合:它的状态客户端按 ID 读取一个聚合,它的查询客户端对它所有的快照或事件流运行查询。查询 DSL @ahoo-wang/wow-client/dsl 构建查询,并以聚合状态的字段提供类型。本页使用 eShop 生成的 ordering 客户端(生成客户端)。
读取一个聚合
const order = await orderStates.get('order-1');
console.log(order.version, order.state.status, order.state.totalAmount);视图包含状态,以及聚合的 ref、version、scope、tags 和它是否 deleted。不存在的聚合以 404 wow.aggregate.not-found reject。totalAmount 在服务端是 BigDecimal,所以在这里是 string,数字与写入时完全相同。
选项可以选择另一个状态:
| 选项 | 读取 |
|---|---|
minVersion | 这个版本或更新的最新状态:写后即可查 |
version | 在这个版本提交的状态 |
at | 截至这个时刻的状态,ISO-8601 文本或 Date |
命令之后,用它的结果给出的版本读取。落后的快照会先用日志追上来:
const placed = await orders.submitOrder({
items: [{ productId: 'p-1', quantity: 1, unitPrice: '19.90' }],
});
const order = await orderStates.get(placed.ref.id, {
minVersion: placed.version ?? 1,
});聚合没有及时达到的版本,以 409 wow.query.min-version-not-reached reject;problem 的 version 是当前的版本。
read 以状态的实体标签 etag 回答;把它作为 ifNoneMatch 传回去,没有变化的状态会回答不带内容的 { notModified: true }。trace(id, { from, to, limit }) 读取其间的提交,每次提交带着它的事件和之后的状态:默认每页 100 次,最多 1,000 次。
查询快照
<Aggregate>SnapshotQueryClient 在 POST /<context>/<aggregate>/snapshot/query 上查询聚合的快照。快照记录把状态放在 state 下,旁边是系统字段(aggregateId、version、ownerId、tags、lastEventTime 等),所以过滤要指明 state.status,而不是 status:
import { desc, filter, pagedQuery } from '@ahoo-wang/wow-client/dsl';
const page = await orderQueries.paged(
pagedQuery({
filter: filter.and([
filter.isIn('state.status', ['PAID', 'SHIPPED']),
filter.afterNow('lastEventTime', '-P7D'),
]),
sort: [desc('lastEventTime')],
pagination: { index: 1, size: 20 },
}),
);
console.log(page.total, page.list.map(record => record.state.status));字段名的类型来自生成的 <Aggregate>StateField 和系统字段,所以拼错的字段无法编译。客户端的每个方法运行一种查询:
| 方法 | 返回 |
|---|---|
single | 唯一匹配的那条记录;没有匹配时 404 wow.query.not-found |
list | 至多 limit 条记录;没有时取服务的默认上限 |
paged | 一页,{ total, exact, list } |
cursor | 一页,以及下一页的 nextCursor,最后一页之后为 null |
count | { count, exact } |
aggregate | 聚合的行:分组和指标 |
listStream | 以服务端推送事件逐条返回记录 |
describe | 查询模型的描述符:字段、每个字段接受的操作符、排序和聚合,以及上限 |
每个操作符、每种请求及其回答的定义,见查询语言。
投影收窄结果
projection.include 列出字面路径的查询,返回只带这些路径的记录,结果的类型也会这样说明:
import { filter } from '@ahoo-wang/wow-client/dsl';
const statuses = await orderQueries.list({
filter: filter.eq('state.status', 'PAID'),
projection: { include: ['aggregateId', 'state.totalAmount'] },
limit: 50,
});
for (const record of statuses) console.log(record.aggregateId, record.state.totalAmount);这里 record.state.status 无法编译:它没有被包含。收窄要求查询写在调用中,并且路径是字面量。用 listQuery 或 pagedQuery 构建的查询,或只有 exclude 的查询,在类型上返回完整的记录。
相对时间与时区
filter.beforeNow 和 filter.afterNow 把字段与服务器时钟加上一个偏移量比较,从不使用浏览器的时钟。日历过滤(today、thisWeek、lastMonth 等)在某个时区中读取它们的日期:你传入的 zoneId,否则是 UTC。要按用户自己的日期,就传入用户的时区:
import { filter } from '@ahoo-wang/wow-client/dsl';
const zoneId = Intl.DateTimeFormat().resolvedOptions().timeZone;
const placedToday = filter.today('firstEventTime', { zoneId });长列表
listStream 以服务端推送事件读取列表,用于长到无法在一次响应中返回的列表。当服务为了让它续读而结束流、连接被切断,或者以可重试的 problem 失败时,客户端会从它读到的最后一条记录之后续读(Last-Event-ID),所以读者对每条记录只看到一次:
import { filter, listQuery } from '@ahoo-wang/wow-client/dsl';
const stream = await orderQueries.listStream(
listQuery({ filter: filter.eq('state.status', 'SHIPPED'), limit: 10_000 }),
);
const reader = stream.getReader();
for (;;) {
const { value, done } = await reader.read();
if (done) break;
console.log(value.aggregateId);
}客户端无法续读的失败,会以它的 WowProblemError 让流出错。
查询事件流
<Aggregate>EventQueryClient 对聚合的事件流运行同样的查询,每次提交一条记录,它的事件在 events 中,类型是生成的 <Aggregate>Event 联合类型:
import { asc, filter, listQuery } from '@ahoo-wang/wow-client/dsl';
const commits = await orderEvents.list(
listQuery({
filter: filter.eq('aggregateId', 'order-1'),
sort: [asc('version')],
}),
);
for (const commit of commits)
for (const event of commit.events) console.log(commit.version, event.name);较旧的事件按它当前的修订号回答,以服务升级它的方式升级(查询事件流)。
查询数据集
数据集是服务为分析而发布的表(从服务发布数据集)。数据集在运行时注册,所以不在 Manifest 中,也没有生成的客户端:DatasetClient 列出调用方能看到的数据集,并按名字查询其中一个:
import { Fetcher } from '@ahoo-wang/fetcher';
import { DatasetClient } from '@ahoo-wang/wow-client';
import { filter } from '@ahoo-wang/wow-client/dsl';
const datasets = new DatasetClient({
fetcher: new Fetcher({ baseURL: 'https://ordering.example.com' }),
});
const sales = datasets.dataset<{ status: string; totalAmount: number }>(
'eshop-ordering',
'sales',
);
const rows = await sales.list({ filter: filter.matchAll(), limit: 100 });
console.log(rows.length);数据集查询在 10.0 中是实验性的:它的 API 仍可能改变。
查询失败时
被拒绝的查询以 WowProblemError reject。400 wow.query.invalid 为它违反的每条规则列出一个 violation:它的 code 是 QueryErrorCodes(来自 /dsl)之一,以及它的 path 和一个 message。这份列表只会增加,所以对你不处理的错误码,退回到 problem 的 detail。429 wow.query.busy 表示存储正在运行它所允许的最多查询:error.retry.afterMs 说明何时再试。每个方法的最后一个参数都是请求选项,其中的 signal 用于取消查询。
React 组件通过 @ahoo-wang/wow-react 的 hooks 运行这些查询,hooks 保存查询、它的结果和加载状态。