У меня есть ответ на запрос graphql формы
{
table {
id
legs {
id
}
}
Это нормализуется для записей table и leg в моем InMemoryCache.
Но тогда, если мое приложение извлекает leg из кеша и ему нужно знать соответствующий table, как мне его найти?
У меня были две идеи:
table к каждому leg, когда приходит ответ на запрос - не уверен, будет ли / как это работать (у меня есть несколько запросов и мутаций, содержащих фрагмент graphql с вышеуказанной формой)cache redirect, но я не знаю, как это сделать, не выполнив поиск tables по всем leg.Предоставляет ли apollo какие-либо функции, подходящие для выполнения этого обратного поиска?
Обновлять: чтобы уточнить, у leg есть опора table, но я, поскольку у меня уже есть информация в клиенте до разрешения этой опоры, я бы хотел разрешить эту опору на стороне клиента, а не на стороне сервера.


Вы должны добавить опору 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> </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> </li>
</ul>
);
})}
</ul>
);
}}
</Query>
);
}
Извините, я неправильно понял ваш вопрос. Затем я предлагаю вам использовать ваш первый вариант, но с client.writeFragment Apollo, потому что это рекомендуемый способ записи произвольных данных в кеш памяти.
Хорошо, но тогда возникает вопрос, какой крючок жизненного цикла подходит для вызова writeFragment, учитывая вышеупомянутую ситуацию? 1. Какую ловушку жизненного цикла <Query> можно использовать? 2. Есть ли что-то еще лучше, учитывая, что несколько запросов разделяют фрагмент?
1. Я бы не использовал <Query>, а <ApolloConsumer>, потому что вам нужен детальный контроль с жизненным циклом запуска запроса. 2. Если я правильно понял, цель состоит в том, чтобы заполнить кеш как можно скорее, поэтому другие запросы должны использовать только кеш, поэтому используйте writeFragment на правильной плитке. Я создам кодовый ящик для демонстрации
Да, лучше всего как можно скорее / для других запросов. Спасибо, я не знал ApolloConsumer. С нетерпением жду, где вы разместите этот компонент. (Возможно, вокруг любых вычислений запросов / мутаций, для которых набор выбора импортирует этот фрагмент, или даже более элегантно?)
Я планирую вызвать в writeFragment после того, как будет выполнен нормализующий запрос, чтобы заполнить отношение leg -> table. Таким образом, обычный запрос, который выбирает table -> legs -> table -> table, должен иметь возможность использовать только кеш. Я не уверен на 100%, что это сработает: s
Надо попробовать :)
Я понял, что это сработало, и соответственно отредактировал ответ :)
Один быстрый вопрос для подтверждения: изменение магазина после выполнения запроса - конечно, хорошая идея. Но если у меня есть несколько запросов, содержащих один и тот же фрагмент в произвольных местах - абстрагироваться от этого невозможно, и мне нужно будет добавить эту настраиваемую логику к каждому запросу, содержащему выборку graphql, импортирующую этот фрагмент table, правильно?
Не обязательно, обратите внимание в демонстрационном коде, как я использовал перенаправление кеша для сопоставления запроса planet(id: "someID") с записью кеша Planet:someID, чтобы клиенту не приходилось выполнять сетевой вызов. Если вы можете предоставить правильные перенаправления кеша, вы можете сделать это только на стороне клиента.
legsуже имеет опоруtable, я просто хочу разрешить эту опору на стороне клиента, а не на стороне сервера.