Dados relacionais
O Reactive Data Client lida com relacionamentos um-para-um, muitos-para-um e muitos-para-muitos em entities usando Entity.schema
Aninhamento
Os membros aninhados são extraídos (hoisted) durante a normalização quando Entity.schema é definido. Eles são então reunidos novamente durante a desnormalização
Diagrama
[{"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"}}]}]
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, });
Joins no lado do cliente
Aninhando dados quando o seu endpoint não aninha.
Mesmo que as respostas da rede não aninhem os dados, podemos realizar joins no lado do cliente especificando o relacionamento em Entity.schema
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, });
Joins baseados em chave
Para cenários mais complexos em que as entities relacionadas são buscadas separadamente, use Entity.process() para criar uma chave de referência que aponte para outra Entity. Isso é útil quando:
- Os dados relacionados vêm de endpoints de API diferentes
- Você quer evitar buscar dados aninhados em excesso
- O relacionamento é opcional ou varia conforme o contexto
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,
};
}
Quando CurrencyResource.getList e StatsResource.getList são ambos buscados, o campo stats
será resolvido automaticamente para a entity Stats correspondente.
Exemplo de preço de criptomoedas
Aqui queremos ordenar Currencies pelo volume de negociação. No entanto, o volume de negociação só está disponível na Entity
Stats. Embora o fetch de CurrencyResource.getList não inclua Stats na resposta, podemos adicionalmente
chamar StatsResource.getList, adicionando-o ao Currency's Entity.schema, o que permite
incluir Stats na Entity Currency e, assim, ordenar com:
entries.sort((a, b) => {
return b?.stats?.volume_usd - a?.stats?.volume_usd;
});
Explore o exemplo coin-app
Buscas reversas
Aninhando dados quando o seu endpoint não aninha (parte 2).
Mesmo que uma resposta aninhe os dados em apenas uma direção, o Reactive Data Client consegue lidar com relacionamentos reversos sobrescrevendo Entity.process. Além disso, pode ser necessário sobrescrever Entity.merge para garantir o merge profundo desses campos esperados.
Isso permite percorrer o relacionamento após processar apenas uma requisição de fetch, em vez de precisar buscar toda vez que você quiser acessar uma visão diferente.
[{"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"}}]}]
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, });
Dependências circulares
Como imports circulares e definições circulares de classes não são permitidos, às vezes será necessário definir o schema depois da definição das Entities.
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],
};
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,
};
}