跳到主要内容

Entity

{
Article: {
'1': {
id: '1',
title: 'Entities define data',
}
}
}

Entity 定义一个_唯一_的对象。

Entity.key + Entity.pk()(主键)使 store 可以采用扁平查找表结构,从而实现高性能、数据一致性和原子变更。

通过定义 schema 等静态成员并重写生命周期方法, Entities 可以自定义数据处理的生命周期。

用法​

import { Entity } from '@data-client/rest';
import { User } from './User';

export class Article extends Entity {
  id = '';
  title = '';
  content = '';
  author = User.fromJS();
  tags: string[] = [];
  createdAt = Temporal.Instant.fromEpochMilliseconds(0);

  static key = 'Article';
  pk() {
    return this.id;
  }

  static schema = {
    author: User,
    createdAt: Temporal.Instant.from,
  };
}

static schema 是对需要处理的字段的声明式定义。在本例中,author 是另一个需要提取的 Entity,而 createdAt 会从字符串转换为 Date 对象。

提示

Entity 通过 resource.schema 或 RestEndpoint.schema 绑定到 Endpoint

提示

如果你已经定义好了自己的类,也可以使用 EntityMixin 来创建 Entity。

重写其他静态成员可以自定义数据的生命周期,如下所示。

成员​

pk(parent?, key?, args?): string | number | undefined​

pk 代表主键,用于唯一标识一个 Entity 实例。默认情况下,它返回 Entity 的 id 字段。

重写此方法可以使用其他字段,或用于其他情况,例如多列主键。

undefined 值​

可以使用 undefined 作为默认值,表示该 Entity 尚未创建。这在直接使用 Entity.fromJS() 初始化创建表单时很有用。如果 pk() 返回 undefined,则认为它尚未持久化到服务器,因此不会保留在缓存中。

其他用途​

由于 pk() 是唯一的,它为定义 JSX 列表 key 提供了一种一致的方式

//....
return (
<div>
{results.map(result => (
<TheThing key={result.pk()} thing={result} />
))}
</div>
);

复合主键​

当单个字段不足以唯一标识一个 Entity 时,你可以将多个字段组合成复合键。这在嵌套资源或具有多段标识符的资源中很常见。

export class Issue extends Entity {
number = 0;
owner = '';
repo = '';
repositoryUrl = '';
title = '';

pk() {
// Composite key from owner, repo, and issue number
return `${this.owner}/${this.repo}/${this.number}`;
}

static key = 'Issue';
}

当 Entity 数据没有直接包含键的所有组成部分时,你可以使用 Entity.process() 从相关字段或 endpoint 参数中提取它们:

export class Issue extends Entity {
number = 0;
owner = '';
repo = '';
repositoryUrl = ''; // Contains: https://api.github.com/repos/{owner}/{repo}
title = '';

pk() {
// Use owner/repo from process() which extracts from repositoryUrl
return `${this.owner}/${this.repo}/${this.number}`;
}

static key = 'Issue';

static process(input: any, parent: any, key: string, args: any[]) {
// Extract owner and repo from the repositoryUrl
const match = input.repositoryUrl?.match(/repos\/([^/]+)\/([^/]+)/);
const owner = args[0]?.owner ?? match?.[1];
const repo = args[0]?.repo ?? match?.[2];
return { ...input, owner, repo };
}
}

单例 Entity​

如果在整个应用中某个 Entity 永远只有一个实例呢?你其实不需要区分各个实例,因此 API 中很可能没有定义 id 或类似的字段。这种情况下,你可以直接返回一个字面量,例如 'the_only_one'。

pk() {
return 'the_only_one';
}

假设你有

const get = new RestEndpoint({
path: '/options',
schema: OptionsEntity,
});
export const OptionsResource = {
get,
partialUpdate: get.extend({ method: 'PATCH' }),
};

static key: string​

它定义的是 Entity 类型的 key,而不是某个实例的 key。它必须是全局唯一的值。

注意

它默认为 this.name;但在会更改类名的生产构建中,这可能会失效。这通常称为类名混淆(mangling)。

这种情况下,你可以重写 key,或禁用类名混淆。

class User extends Entity {
id = '';
username = '';

pk() {
return this.id;
}
static key = 'User';
}

static schema: { [k: keyof this]: Schema }​

定义关联 Entity 成员,或字段反序列化,例如 Date 和 BigNumber。

Fixtures
GET /posts/123
{"id":"5","author":{"id":"123","name":"Jim"},"content":"Happy day","createdAt":"2019-01-23T06:07:48.311Z"}
▶User
▶Post
import { Entity } from '@data-client/rest';
import { Temporal } from 'temporal-polyfill';
import { User } from './User';

export class Post extends Entity {
  id = '';
  author = User.fromJS();
  createdAt = Temporal.Instant.fromEpochMilliseconds(0);
  content = '';
  title = '';

  pk() {
    return this.id;
  }
  static key = 'Post';

  static schema = {
    author: User,
    createdAt: Temporal.Instant.from,
  };
}
▶PostPage
结果
Store▶

可选成员​

此处引用的 Entity,如果其在 Record 定义中的默认值本身就是空值,则被视为“可选”

class User extends Entity {
friend: User | null = null; // this field is optional
lastUpdated = Temporal.Instant.fromEpochMilliseconds(0);

static schema = {
friend: User,
lastUpdated: Temporal.Instant.from,
};
}

static indexes?: (keyof this)[]​

索引可以提升基于这些参数进行查找时的性能。将之后想要作为查找参数发送的字段名(例如 slug、username)添加到列表中。

备注

不要把 id 之类的主键加入索引列表,因为它已经被优化过了。

useSuspense()​

配合 useSuspense(),它会尽可能从 Entity 表中提前推断出结果,无需等待 fetch 完成即可渲染。当 Entity 缓存已经由其他请求(例如列表请求)填充时,这通常很有帮助。

export class User extends Entity {
id: number | undefined = undefined;
username = '';
email = '';
isAdmin = false;

static indexes = ['username' as const];
}
export const UserResource = resource({
path: '/user/:id',
schema: User,
});
import { useSuspense } from '@data-client/react';
import { UserResource } from './resources/User';

const user = useSuspense(UserResource.get, { username: 'bob' });

useQuery()​

配合 useQuery(),它让你可以访问在其他请求中获取到的结果——即使没有可以获取它的 endpoint。

class LatestPrice extends Entity {
id = '';
symbol = '';
price = '0.0';

static indexes = ['symbol' as const];
}
class Asset extends Entity {
id = '';
price = '';

static schema = {
price: LatestPrice,
};
}
const getAssets = new RestEndpoint({
path: '/assets',
schema: [Asset],
});

某个顶层组件:

import { useSuspense } from '@data-client/react';
import { getAssets } from './resources/Asset';

const assets = useSuspense(getAssets);

嵌套在下层:

import { useQuery } from '@data-client/react';
import { LatestPrice } from './resources/LatestPrice';

const price = useQuery(LatestPrice, { symbol: 'BTC' });

static maxEntityDepth?: number​

在反规范化期间限制 Entity 的嵌套深度,以防止在大型双向 Entity 图中发生栈溢出。默认值:64

当双向关系形成包含大量唯一 Entity 的链条时(例如 Department → Building → Department → ...),反规范化可能会递归数千层。maxEntityDepth 会在指定深度截断解析—— 超出限制的 Entity 在返回时,其嵌套的外键会保留为未解析的 id,而不是完全反规范化的对象。

class Department extends Entity {
id = '';
name = '';
buildings: Building[] = [];

pk() {
return this.id;
}
static key = 'Department';
static maxEntityDepth = 16;

static schema = {
buildings: [Building],
};
}
提示

请在参与深层或宽泛双向关系的 Entity 上设置此项。普通的 Entity 图(深度 < 10)永远不会接近默认限制。

对于不需要立即反规范化的关系,Lazy 会完全跳过解析,让你通过 useQuery 按需解析。

生命周期​

static fromJS(props): Entity​

把 props 复制到新实例的工厂方法。请用它代替 new MyEntity(),以确保默认 props 会被覆盖。

static process(input, parent, key, args): processedEntity​

在规范化该 Entity 的开始阶段运行。返回值会保存到 store 中,并传给 pk()。

默认只是简单地复制响应({...input})

如何通过覆盖它来为关系型数据构建反向查找

缺少 id 的情况​

class Stream extends Entity {
username = '';
title = '';
game = '';
currentViewers = 0;
live = false;

pk() {
return this.username;
}
static key = 'Stream';

static process(value, parent, key, args) {
// super.process creates a copy of value
const processed = super.process(value, parent, key, args);
processed.username = args[0]?.username;
return processed;
}
}

动态失效​

从 Entity.process 返回 undefined 会使该 Entity 失效。这让我们可以根据具体的响应数据,动态地进行失效。

class PriceLevel extends Entity {
price = 0;
amount = 0;

pk() {
return this.price;
}

static process(
input: [number, number],
parent: any,
key: string | undefined,
): any {
const [price, amount] = input;
if (amount === 0) return undefined;
return { price, amount };
}
}

static mergeWithStore(existingMeta, incomingMeta, existing, incoming): mergedValue​

static mergeWithStore(
existingMeta: {
date: number;
fetchedAt: number;
},
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
const shouldUpdate = this.shouldUpdate(
existingMeta,
incomingMeta,
existing,
incoming,
);

if (shouldUpdate) {
// distinct types are not mergeable (like delete symbol), so just replace
if (typeof incoming !== typeof existing) {
return incoming;
} else {
return this.shouldReorder(
existingMeta,
incomingMeta,
existing,
incoming,
)
? this.merge(incoming, existing)
: this.merge(existing, incoming);
}
} else {
return existing;
}
}

当规范化过程中发现 store 中已存在某个处理过的 Entity 时,会调用 mergeWithStore()。

它会调用 shouldUpdate()、shouldReorder(),并可能调用 merge()

static shouldUpdate(existingMeta, incomingMeta, existing, incoming): boolean​

static shouldUpdate(
existingMeta: { date: number; fetchedAt: number },
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
return existingMeta.fetchedAt <= incomingMeta.fetchedAt;
}

阻止更新​

shouldUpdate 也可以用来短路 Entity 的更新。

import deepEqual from 'deep-equal';

class Article extends Entity {
id = '';
title = '';
content = '';
published = false;

static shouldUpdate(
existingMeta: { date: number; fetchedAt: number },
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
return !deepEqual(incoming, existing);
}
}

static shouldReorder(existingMeta, incomingMeta, existing, incoming): boolean​

static shouldReorder(
existingMeta: { date: number; fetchedAt: number },
incomingMeta: { date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
return incomingMeta.fetchedAt < existingMeta.fetchedAt;
}

返回 true 会在 merge 中调换传入 Entity 与 store 中 Entity 的参数顺序。在默认的 merge 下,这会让已有 Entity 的字段覆盖传入 Entity 的字段,而不是反过来。

示例​

class LatestPriceEntity extends Entity {
  id = '';
  updatedAt = 0;
  price = '0.0';
  symbol = '';

  pk() {
    return this.id;
  }

  static shouldReorder(
    existingMeta: { date: number; fetchedAt: number },
    incomingMeta: { date: number; fetchedAt: number },
    existing: { updatedAt: number },
    incoming: { updatedAt: number },
  ) {
    return incoming.updatedAt < existing.updatedAt;
  }
}

更多演示

static merge(existing, incoming): mergedValue​

static merge(existing: any, incoming: any) {
return {
...existing,
...incoming,
};
}

Merge 用于处理传入的 Entity 已经存在的情况。当同一个响应中出现相同的 Entity 时,会直接调用它。默认情况下,当 mergeWithStore() 判定传入的 Entity 应与已持久化在 Reactive Data Client store 中的 Entity 合并时,也会调用它。

如何通过覆盖它来为关系型数据构建反向查找

static mergeMetaWithStore(existingMeta, incomingMeta, existing, incoming): meta​

static mergeMetaWithStore(
existingMeta: {
expiresAt: number;
date: number;
fetchedAt: number;
},
incomingMeta: { expiresAt: number; date: number; fetchedAt: number },
existing: any,
incoming: any,
) {
return this.shouldReorder(existingMeta, incomingMeta, existing, incoming)
? existingMeta
: incomingMeta;
}

当规范化过程中发现 store 中已存在某个处理过的 Entity 时,会调用 mergeMetaWithStore()。

static queryKey(args, queryKey, getEntity, getIndex): pk?​

这个方法让 Entities 成为 Queryable,从而无需 endpoint 即可访问 store。

覆盖它可以定制这一行为,或者完全禁用它。

返回 undefined 会禁止这一行为。

返回 pk 字符串时,会尝试查找该 Entity 并在响应中使用它。

使用时,过期策略会根据该 Entity 自身的元数据计算。

默认使用第一个参数在 pk() 和 indexes 中查找

getEntity(key, pk?)​

传入一个参数时获取某一类型的所有 Entity,传入两个参数时获取单个 Entity

One argument
const entitiesEntry = getEntity(this.schema.key);
if (entitiesEntry === undefined) return INVALID;
return Object.values(entitiesEntry).map(
entity => entity && this.schema.pk(entity),
);
Two arguments
if (getEntity(this.key, id)) return id;

getIndex(key, indexName, value)​

返回索引条目(value->pk 映射)

const value = args[0][indexName];
return getIndex(schema.key, indexName, value)[value];

static createIfValid(processedEntity): Entity | undefined​

在反规范化 Entity 时调用。如果该 Entity 被认为是“有效”的,就会创建这个类的实例。

返回 undefined 会导致 Invalid 过期状态,与 Invalidate 类似。

Invalid 过期状态通常意味着 hook 会进入加载状态并尝试重新获取。

static createIfValid(props): AbstractInstanceType<this> | undefined {
if (this.validate(props)) {
return undefined as any;
}
return this.fromJS(props);
}

static validate(processedEntity): errorMessage?​

在规范化和反规范化期间都会运行。返回字符串表示出错(该字符串就是错误消息)。

在规范化期间,校验失败会导致该次获取产生错误。

在反规范化期间,校验失败会把该结果标记为“无效”,从而阻塞直到获取到结果。

默认仅在开发模式下做一些基本的字段存在性检查。可以覆盖它来禁用或定制。

为字段不完整的 endpoint 使用校验