Datos relacionales
Reactive Data Client maneja relaciones uno a uno, muchos a uno y muchos a muchos entre entities mediante Entity.schema
Anidamiento
Los miembros anidados se elevan durante la normalización cuando se define Entity.schema. Luego se vuelven a unir durante la desnormalización
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 del lado del cliente
Anidar datos cuando tu endpoint no lo hace.
Aunque las respuestas de red no anidan los datos, podemos realizar joins del lado del cliente especificando la relación en 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 basados en claves
Para escenarios más complejos donde las entities relacionadas se obtienen por separado, usa Entity.process() para crear una clave de referencia que enlace con otra Entity. Esto es útil cuando:
- Los datos relacionados provienen de distintos endpoints de la API
- Quieres evitar obtener datos anidados en exceso
- La relación es opcional o varía según el 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,
};
}
Cuando se obtienen tanto CurrencyResource.getList como StatsResource.getList, el campo stats
se resolverá automáticamente a la entity Stats correspondiente.
Ejemplo de precios de criptomonedas
Aquí queremos ordenar las Currencies por su volumen de operaciones. Sin embargo, el volumen de operaciones solo está disponible en la Entity
Stats. Aunque el fetch de CurrencyResource.getList no incluye Stats en la respuesta, podemos llamar además
a StatsResource.getList y agregarlo al Entity.schema de Currency's, lo que permite
incluir Stats en nuestra Entity Currency, y eso habilita el ordenamiento con:
entries.sort((a, b) => {
return b?.stats?.volume_usd - a?.stats?.volume_usd;
});
Explora el ejemplo coin-app
Búsquedas inversas
Anidar datos cuando tu endpoint no lo hace (parte 2).
Aunque una respuesta solo anide en una dirección, Reactive Data Client puede manejar las relaciones inversas sobrescribiendo Entity.process. Además, puede ser necesario sobrescribir Entity.merge para garantizar la combinación profunda de esos campos esperados.
Esto te permite recorrer la relación después de procesar una sola solicitud de fetch, en lugar de tener que hacer un fetch cada vez que quieras acceder a una vista distinta.
[{"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, });
Dependencias circulares
Como no se permiten las importaciones circulares ni las definiciones circulares de clases, a veces será necesario definir el schema después de la definición de las 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,
};
}