跳到主要内容

关系型数据

Reactive Data Client 通过 Entity.schema 处理 Entity 之间的一对一、多对一和多对多关系

嵌套​

定义了 Entity.schema 后,嵌套成员会在规范化时被提升出来,然后在反规范化时重新组合

示意图
 
Fixtures
GET /posts
[{"id":"1","title":"My first post!","author":{"id":"123","name":"Paul"},"comments":[{"id":"249","content":"Nice post!","commenter":{"id":"245","name":"Jane"}},{"id":"250","content":"Thanks!","commenter":{"id":"123","name":"Paul"}}]},{"id":"2","title":"This other post","author":{"id":"123","name":"Paul"},"comments":[{"id":"251","content":"Your other post was nicer","commenter":{"id":"245","name":"Jane"}},{"id":"252","content":"I am a spammer!","commenter":{"id":"246","name":"Spambot5000"}}]}]
▶resources/Post
import { Collection, Entity, resource } from '@data-client/rest';

export class User extends Entity {
  id = '';
  name = '';
}

export class Comment extends Entity {
  id = '';
  content = '';
  commenter = User.fromJS();

  static schema = {
    commenter: User,
  };
}

export class Post extends Entity {
  id = '';
  title = '';
  author = User.fromJS();
  comments: Comment[] = [];

  static schema = {
    author: User,
    comments: new Collection([Comment], {
      nestKey: (parent, key) => ({
        postId: parent.id,
      }),
    }),
  };
}

export const PostResource = resource({
  path: '/posts/:id',
  schema: Post,
});
▶PostPage
结果
Store▶

客户端 join​

在 endpoint 不嵌套数据时实现嵌套。

即使网络响应中的数据没有嵌套,我们也可以通过在 Entity.schema 中指定关联关系来进行客户端 join

▶resources/User
▶resources/Todo
import { Entity, resource } from '@data-client/rest';
import { User } from './User';

export class Todo extends Entity {
  id = 0;
  userId = 0;
  user? = User.fromJS();
  title = '';
  completed = false;
  static schema = {
    user: User,
  };
  static process(todo) {
    return { ...todo, user: todo.userId };
  }
}
export const TodoResource = resource({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/todos/:id',
  schema: Todo,
});
▶TodoJoined
结果
Store▶

基于键的 join​

在关联 Entity 是分别获取的更复杂场景中,可以使用 Entity.process() 创建一个指向另一个 Entity 的引用键。这在以下情况中很有用:

  • 关联数据来自不同的 API endpoint
  • 你想避免过度获取嵌套数据
  • 关联关系是可选的,或随上下文而变化
import { Entity, resource } from '@data-client/rest';

class Stats extends Entity {
product_id = '';
volume = 0;
price = 0;

pk() {
return this.product_id;
}

static key = 'Stats';
}

class Currency extends Entity {
id = '';
name = '';
// Default value allows Currency to exist without Stats loaded
stats = Stats.fromJS();

pk() {
return this.id;
}

static key = 'Currency';

// Create a reference key that links to Stats entity
static process(input: any, parent: any, key: string, args: any[]) {
// The stats field becomes a reference to Stats with pk `${id}-USD`
return { ...input, stats: `${input.id}-USD` };
}

static schema = {
// Stats will be looked up by the key from process()
stats: Stats,
};
}

当 CurrencyResource.getList 和 StatsResource.getList 都获取完成后,stats 字段会自动解析为对应的 Stats Entity。

加密货币价格示例​

这里我们想按交易量对 Currencies 排序。但交易量只存在于 Stats Entity 中。尽管 CurrencyResource.getList 的响应中不包含 Stats,我们仍然可以额外调用 StatsResource.getList,同时把它加入 Currency's Entity.schema——这样 Currency Entity 中就包含了 Stats,从而可以这样排序:

entries.sort((a, b) => {
return b?.stats?.volume_usd - a?.stats?.volume_usd;
});

更多演示

反向查找​

在 endpoint 不嵌套数据时实现嵌套(第二部分)。

即使响应只在一个方向上嵌套,Reactive Data Client 也能通过覆盖 Entity.process 处理反向关系。此外,可能还需要覆盖 Entity.merge,以确保对这些预期字段进行深度合并。

这样你只需处理一次获取请求就能遍历这些关联关系,而不必每次想访问不同视图时都重新获取。

Fixtures
GET /posts
[{"id":"1","title":"My first post!","author":{"id":"123","name":"Paul"},"comments":[{"id":"249","content":"Nice post!","commenter":{"id":"245","name":"Jane"}},{"id":"250","content":"Thanks!","commenter":{"id":"123","name":"Paul"}}]},{"id":"2","title":"This other post","author":{"id":"123","name":"Paul"},"comments":[{"id":"251","content":"Your other post was nicer","commenter":{"id":"245","name":"Jane"}},{"id":"252","content":"I am a spammer!","commenter":{"id":"246","name":"Spambot5000"}}]}]
▶resources/Post
import { Entity, resource, type Schema } from '@data-client/rest';

export class User extends Entity {
  id = '';
  name = '';
  posts: Post[] = [];
  comments: Comment[] = [];

  static merge(existing, incoming) {
    return {
      ...existing,
      ...incoming,
      posts: [...(existing.posts || []), ...(incoming.posts || [])],
      comments: [
        ...(existing.comments || []),
        ...(incoming.comments || []),
      ],
    };
  }

  static process(value, parent, key) {
    switch (key) {
      case 'author':
        return { ...value, posts: [parent.id] };
      case 'commenter':
        return { ...value, comments: [parent.id] };
      default:
        return { ...value };
    }
  }
}

export class Comment extends Entity {
  id = '';
  content = '';
  commenter = User.fromJS();
  post = Post.fromJS();

  static schema: Record<string, Schema> = {
    commenter: User,
  };
  static process(value, parent, key) {
    return { ...value, post: parent.id };
  }
}

export class Post extends Entity {
  id = '';
  title = '';
  author = User.fromJS();
  comments: Comment[] = [];

  static schema = {
    author: User,
    comments: [Comment],
  };
}

// with cirucular dependencies we must set schema after they are all defined
User.schema = {
  posts: [Post],
  comments: [Comment],
};
Comment.schema = {
  ...Comment.schema,
  post: Post,
};

export const PostResource = resource({
  path: '/posts/:id',
  schema: Post,
  dataExpiryLength: Infinity,
});
export const UserResource = resource({
  path: '/users/:id',
  schema: User,
});
▶UserPage
▶PostPage
▶Navigation
结果
Store▶

循环依赖​

由于不允许循环导入和循环的类定义,有时需要在 Entity 定义之后再定义 schema。

resources/Post
import { Collection, Entity } from '@data-client/rest';
import { User } from './User';

export class Post extends Entity {
id = '';
title = '';
author = User.fromJS();

static schema = {
author: User,
};
}

// both User and Post are now defined, so it's okay to refer to both of them
User.schema = {
// ensure we keep the 'createdAt' member
...User.schema,
posts: [Post],
};
resources/User
import { Collection, Entity } from '@data-client/rest';
import type { Post } from './Post';
// we can only import the type else we break javascript imports
// thus we change the schema of UserResource above

export class User extends Entity {
id = '';
name = '';
posts: Post[] = [];
createdAt = Temporal.Instant.fromEpochMilliseconds(0);

static schema: Record<string, Schema | Date> = {
createdAt: Temporal.Instant.from,
};
}
提示

对于不需要立即反规范化的双向关系, Lazy 会延迟解析,让你可以通过 useQuery 按需解析,从而避免深度递归,并改善记忆化的隔离性。