Saltar al contenido principal

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
 
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
Resultado
Store▶

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

▶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
Resultado
Store▶

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

More Demos

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.

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
Resultado
Store▶

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.

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,
};
}
consejo

Para las relaciones bidireccionales que no necesitan desnormalización anticipada, Lazy difiere la resolución y te permite resolver bajo demanda mediante useQuery, evitando la recursión profunda y mejorando el aislamiento de la memoización.