Apollo-client: Как получить обратную зависимость из кеша?

У меня есть ответ на запрос graphql формы

{
  table {
    id
    legs {
      id
    }
  }

Это нормализуется для записей table и leg в моем InMemoryCache.

Но тогда, если мое приложение извлекает leg из кеша и ему нужно знать соответствующий table, как мне его найти?

У меня были две идеи:

  • добавление опоры table к каждому leg, когда приходит ответ на запрос - не уверен, будет ли / как это работать (у меня есть несколько запросов и мутаций, содержащих фрагмент graphql с вышеуказанной формой)
  • имея подходящий cache redirect, но я не знаю, как это сделать, не выполнив поиск tables по всем leg.

Предоставляет ли apollo какие-либо функции, подходящие для выполнения этого обратного поиска?

Обновлять: чтобы уточнить, у leg есть опора table, но я, поскольку у меня уже есть информация в клиенте до разрешения этой опоры, я бы хотел разрешить эту опору на стороне клиента, а не на стороне сервера.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Что такое Apollo Client и зачем он нужен?
Что такое Apollo Client и зачем он нужен?
Apollo Client - это полнофункциональный клиент GraphQL для JavaScript-приложений, который упрощает получение, управление и обновление данных в...
0
0
622
1

Ответы 1

Вы должны добавить опору table к каждому leg. Согласно документация graphql.org, вы должны думать в виде графиков:

With GraphQL, you model your business domain as a graph by defining a schema; within your schema, you define different types of nodes and how they connect/relate to one another.

В вашей модели столы и ноги являются узлами на графике вашей бизнес-модели. Когда вы добавляете опору таблицы к каждой ноге, вы создаете новое ребро в этом графике, по которому ваш клиентский код может пройти, чтобы получить соответствующие данные.

Редактировать после уточнения:

Вы можете использовать writeFragment и для получения детального контроля над кешем Apollo. После того, как запрос на заполнение кеша будет выполнен, вычислите обратную зависимость и запишите ее в кеш следующим образом:

fetchTables = async () => {
  const client = this.props.client

  const result = await client.query({
    query: ALL_TABLES_QUERY,
    variables: {}
  })

  // compute the reverse link
  const tablesByLeg = {}
  for (const table of result.data.table) {
    for (const leg of table.legs) {
      if (!tablesByLeg[leg.id]) {
        tablesByLeg[leg.id] = {
          leg: leg,
          tables: []
        }
      }
      tablesByLeg[leg.id].tables.push(table)
    }
  }

  // write to the Apollo cache
  for (const { leg, tables } of Object.values(tablesByLeg)) {
    client.writeFragment({
      id: dataIdFromObject(leg),
      fragment: gql`
        fragment reverseLink from Leg {
          id
          tables {
            id
          }
        }
      `,
      data: {
        ...leg,
        tables
      }
    })
  }

  // update component state
  this.setState(state => ({
    ...state,
    tables: Object.values(result)
  }))
}

Демо

Я разместил здесь полный пример: https://codesandbox.io/s/6vx0m346z Я также поместил его ниже для полноты картины.

index.js

import React from "react";
import ReactDOM from "react-dom";
import { ApolloProvider } from "react-apollo";
import { createClient } from "./client";
import { Films } from "./Films";

const client = createClient();

function App() {
  return (
    <ApolloProvider client = {client}>
      <Films />
    </ApolloProvider>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

client.js

import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import { HttpLink } from "apollo-link-http";

export function dataIdFromObject(object) {
  return object.id ? object.__typename + ":" + object.id : null;
}

export function createClient() {
  return new ApolloClient({
    connectToDevTools: true,
    ssrMode: false,
    link: new HttpLink({
      uri: "https://prevostc-swapi-graphql.herokuapp.com"
    }),
    cache: new InMemoryCache({
      dataIdFromObject,
      cacheRedirects: {
        Query: {
          planet: (_, args, { getCacheKey }) =>
            getCacheKey({ __typename: "Planet", id: args.id })
        }
      }
    })
  });
}

Films.js

import React from "react";
import gql from "graphql-tag";
import { withApollo } from "react-apollo";
import { dataIdFromObject } from "../src/client";
import { Planet } from "./Planet";

const ALL_FILMS_QUERY = gql`
  query {
    allFilms {
      films {
        id
        title
        planetConnection {
          planets {
            id
            name
          }
        }
      }
    }
  }
`;

const REVERSE_LINK_FRAGMENT = gql`
  fragment reverseLink on Planet {
    id
    name
    filmConnection {
      films {
        id
        title
      }
    }
  }
`;

class FilmsComponent extends React.Component {
  constructor() {
    super();
    this.state = { films: [], selectedPlanetId: null };
  }

  componentDidMount() {
    this.fetchFilms();
  }

  fetchFilms = async () => {
    const result = await this.props.client.query({
      query: ALL_FILMS_QUERY,
      variables: {}
    });

    // compute the reverse link
    const filmByPlanet = {};
    for (const film of result.data.allFilms.films) {
      for (const planet of film.planetConnection.planets) {
        if (!filmByPlanet[planet.id]) {
          filmByPlanet[planet.id] = {
            planet: planet,
            films: []
          };
        }
        filmByPlanet[planet.id].films.push(film);
      }
    }

    // write to the apollo cache
    for (const { planet, films } of Object.values(filmByPlanet)) {
      this.props.client.writeFragment({
        id: dataIdFromObject(planet),
        fragment: REVERSE_LINK_FRAGMENT,
        data: {
          ...planet,
          filmConnection: {
            films,
            __typename: "PlanetsFilmsConnection"
          }
        }
      });
    }

    // update component state at last
    this.setState(state => ({
      ...state,
      films: Object.values(result.data.allFilms.films)
    }));
  };

  render() {
    return (
      <div>
        {this.state.selectedPlanetId && (
          <div>
            <h1>Planet query result</h1>
            <Planet id = {this.state.selectedPlanetId} />
          </div>
        )}
        <h1>All films</h1>
        {this.state.films.map(f => {
          return (
            <ul key = {f.id}>
              <li>id: {f.id}</li>
              <li>
                title: <strong>{f.title}</strong>
              </li>
              <li>__typename: {f.__typename}</li>
              <li>
                planets:
                {f.planetConnection.planets.map(p => {
                  return (
                    <ul key = {p.id}>
                      <li>id: {p.id}</li>
                      <li>
                        name: <strong>{p.name}</strong>
                      </li>
                      <li>__typename: {p.__typename}</li>
                      <li>
                        <button
                          onClick = {() =>
                            this.setState(state => ({
                              ...state,
                              selectedPlanetId: p.id
                            }))
                          }
                        >
                          select
                        </button>
                      </li>
                      <li>&nbsp;</li>
                    </ul>
                  );
                })}
              </li>
            </ul>
          );
        })}
        <h1>The current cache is:</h1>
        <pre>{JSON.stringify(this.props.client.extract(), null, 2)}</pre>
      </div>
    );
  }
}

export const Films = withApollo(FilmsComponent);

Planet.js

import React from "react";
import gql from "graphql-tag";
import { Query } from "react-apollo";

const PLANET_QUERY = gql`
  query ($id: ID!) {
    planet(id: $id) {
      id
      name
      filmConnection {
        films {
          id
          title
        }
      }
    }
  }
`;

export function Planet({ id }) {
  return (
    <Query query = {PLANET_QUERY} variables = {{ id }}>
      {({ loading, error, data }) => {
        if (loading) return "Loading...";
        if (error) return `Error! ${error.message}`;

        const p = data.planet;
        return (
          <ul key = {p.id}>
            <li>id: {p.id}</li>
            <li>
              name: <strong>{p.name}</strong>
            </li>
            <li>__typename: {p.__typename}</li>
            {p.filmConnection.films.map(f => {
              return (
                <ul key = {f.id}>
                  <li>id: {f.id}</li>
                  <li>
                    title: <strong>{f.title}</strong>
                  </li>
                  <li>__typename: {f.__typename}</li>
                  <li>&nbsp;</li>
                </ul>
              );
            })}
          </ul>
        );
      }}
    </Query>
  );
}
legs уже имеет опору table, я просто хочу разрешить эту опору на стороне клиента, а не на стороне сервера.
bebbi 15.09.2018 22:49

Извините, я неправильно понял ваш вопрос. Затем я предлагаю вам использовать ваш первый вариант, но с client.writeFragment Apollo, потому что это рекомендуемый способ записи произвольных данных в кеш памяти.

Clément Prévost 15.09.2018 23:57

Хорошо, но тогда возникает вопрос, какой крючок жизненного цикла подходит для вызова writeFragment, учитывая вышеупомянутую ситуацию? 1. Какую ловушку жизненного цикла <Query> можно использовать? 2. Есть ли что-то еще лучше, учитывая, что несколько запросов разделяют фрагмент?

bebbi 17.09.2018 11:44

1. Я бы не использовал <Query>, а <ApolloConsumer>, потому что вам нужен детальный контроль с жизненным циклом запуска запроса. 2. Если я правильно понял, цель состоит в том, чтобы заполнить кеш как можно скорее, поэтому другие запросы должны использовать только кеш, поэтому используйте writeFragment на правильной плитке. Я создам кодовый ящик для демонстрации

Clément Prévost 17.09.2018 12:08

Да, лучше всего как можно скорее / для других запросов. Спасибо, я не знал ApolloConsumer. С нетерпением жду, где вы разместите этот компонент. (Возможно, вокруг любых вычислений запросов / мутаций, для которых набор выбора импортирует этот фрагмент, или даже более элегантно?)

bebbi 17.09.2018 17:44

Я планирую вызвать в writeFragment после того, как будет выполнен нормализующий запрос, чтобы заполнить отношение leg -> table. Таким образом, обычный запрос, который выбирает table -> legs -> table -> table, должен иметь возможность использовать только кеш. Я не уверен на 100%, что это сработает: s

Clément Prévost 17.09.2018 18:02

Надо попробовать :)

Clément Prévost 20.09.2018 19:52

Я понял, что это сработало, и соответственно отредактировал ответ :)

Clément Prévost 22.09.2018 23:16

Один быстрый вопрос для подтверждения: изменение магазина после выполнения запроса - конечно, хорошая идея. Но если у меня есть несколько запросов, содержащих один и тот же фрагмент в произвольных местах - абстрагироваться от этого невозможно, и мне нужно будет добавить эту настраиваемую логику к каждому запросу, содержащему выборку graphql, импортирующую этот фрагмент table, правильно?

bebbi 23.09.2018 21:26

Не обязательно, обратите внимание в демонстрационном коде, как я использовал перенаправление кеша для сопоставления запроса planet(id: "someID") с записью кеша Planet:someID, чтобы клиенту не приходилось выполнять сетевой вызов. Если вы можете предоставить правильные перенаправления кеша, вы можете сделать это только на стороне клиента.

Clément Prévost 23.09.2018 21:34

Другие вопросы по теме