GraphQL позволяет указывать поля в ваших запросах. Но получили ли от этого пользу ваш бэкэнд и база данных?

Типичный запрос

Давайте представим, что у вас есть схема GraphQL, которая предоставляет эти поля внутри listBooks запроса.

listContents {
    id
    title
    coverImage
    description    
    content // has a large size as it containing the content in flat text, for searching purpose
    contentHtml // has a large size as it containing the content in HTML format
  }

Как правило, вы должны написать типичный запрос к базе данных, например select * from contents, в Backend. Тогда ваши разработчики внешнего интерфейса в основном будут запрашивать только id, title, coverImage, description для создания списка контента.

Проблема

Конечно, описанный выше подход будет работать. Однако в вашей базе данных и внутреннем API будет неэффективность. Во-первых, ваша база данных должна будет обслуживать тяжелые поля content и contentHtml при выполнении запроса к базе данных. Затем content и contentHtml увеличат размер полезной нагрузки, когда база данных отправит данные на ваш сервер.

Следовательно, ваш бэкенд также будет потреблять значительный объем памяти при запуске ContentEntity в качестве ObjectType GraphQL. Только, в конце концов, мы бы не стали отправлять content и contentHtml, потому что они не нужны нашему интерфейсу. Какой расточительный ресурс.

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

Конечно, вы также можете создать отдельный запрос GraphQL с другим набором fields, чтобы сделать его более эффективным. Но это только усложнит вам задачу в будущем, потому что вам нужно будет поддерживать несколько запросов, если в структуре Content будут какие-либо изменения.

GraphQLResolveInfo для создания более умного запроса к базе данных

Мы могли бы построить интеллектуальный запрос, используя информацию о схеме из GraphQLResolveInfo и объединив ее с пакетом graphql-parse-resolve-info.

Мы назвали его умным, потому что он будет запрашивать базу данных только с запрошенными полями на основе GraphQLResolveInfo. Ниже приведен пример реализации метода find() из TypeORM. Конечно, вы также можете использовать его для различных типов ORM или даже необработанных SQL-запросов.

import { Info, Query, Resolver } from 'type-graphql';

export default class ContentsResolver {
  @Query(() => ContentEntity], { description: 'List all contents' })
  async listContents(@Info() info: GraphQLResolveInfo): Promise<ContentEntity[]> {
    const repository = getRepository(ContentEntity);

    // GraphQL Query `listContents{id, title, coverImage, description}`
    // will generate the following SQL 
    // `select id, title, coverImage, description from contents`
    return await repository.find({
      select: getFlatFieldsFromResolvedInfo(info),     
    })    
  }
}
import { GraphQLResolveInfo } from "graphql";
import { FieldsByTypeName, parseResolveInfo} from "graphql-parse-resolve-info";

const getFlatFieldsFromResolvedInfo = (info: GraphQLResolveInfo
) => {  
  const resolvedInfo = parseResolveInfo(info); 
  const resourceTree: FieldsByTypeName[any] = Object.values(resolvedInfo.fieldsByTypeName)[0];
  return Object.keys(resourceTree)    
}

С помощью вышеуказанного механизма content и contentHtml будут извлекаться только в том случае, если они требуются нашему интерфейсу. Однако этот механизм будет работать только с запросом, который не содержит вложенных полей.

Умный запрос к базе данных для объединенной таблицы

Допустим, у вас есть следующая схема GraphQL с author и category в качестве вложенных полей. Данные author и category извлекаются из таблиц authors и categories соответственно.

listContents {
    id
    title
    coverImage
    description    
    content // has a large size as it containing the content in flat text, for searching purpose
    contentHtml // has a large size as it containing the content in HTML format
    author {
      id
      name
      email
    }
    category {
      id
      name
      coverImage
    }
 }

Чтобы быть эффективным, вам нужно использовать JOIN только в том случае, если требуется соответствующая таблица. Итак, вот код:

import { Info, Query, Resolver } from 'type-graphql';

export default class ContentsResolver {
  @Query(() => ContentEntity], { description: 'List all contents' })
  async listContents(@Info() info: GraphQLResolveInfo): Promise<ContentEntity[]> {
    const repository = getRepository(ContentEntity);

    // GraphQL Query `listContents{id, title, coverImage, author { id, name, email} }`
    // will generate the some kind of the following SQL 
    // `select id, title, coverImage, author.* from contents left join author`
    // You could use raw SQL to make the fields to be more selective on the relation table
    const { flatFields, nestedFields } = getFieldsFromResolvedInfo(info)

    return repository.find({
      select: flatFields,
      loadEagerRelations: false,
      relations: Object.keys(nestedFields),      
    })    
  }
}
import { GraphQLResolveInfo } from 'graphql';
import { FieldsByTypeName, parseResolveInfo, ResolveTree } from 'graphql-parse-resolve-info';

type GraphQLMappedFields = {
  flatFields: string[];
  nestedFields: { [fieldName: string]: GraphQLMappedFields };
};

function isFlatField(tree: ResolveTree) {
  return Object.keys(tree.fieldsByTypeName).length === 0;
}

const fetchResolveInfoNestedFields = (resolvedInfo: ResolveTree | FieldsByTypeName) => {
  const resourceTreeMap: FieldsByTypeName[any] = Object.values(resolvedInfo.fieldsByTypeName)[0];
  return Object.keys(resourceTreeMap).reduce(
    (map: GraphQLMappedFields, field) => {
      if (isFlatField(resourceTreeMap[field])) {
        map.flatFields.push(field);
      } else {
        map.nestedFields[field] = fetchResolveInfoNestedFields(resourceTreeMap[field]);
      }

      return map;
    },
    { flatFields: [], nestedFields: {} }
  );
};

// GraphQL Query `listContents{id, title, coverImage, author { id, name, email} }`
// will output "{ 
//   flatFields: [id, title, coverImage], 
//   nestedFields: {
//      author {
//         flatFields: [id, name, email], 
//         nestedFields: {}
//      }
//   } 
// }
export const getFieldsFromResolvedInfo = (info: GraphQLResolveInfo) => {
  return fetchResolveInfoNestedFields(parseResolveInfo(info));  
};

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

Я не буду раскрывать необработанный механизм SQL, так как в другом случае будет другой подход. Но если у вас есть хорошие рекомендации, пожалуйста, не стесняйтесь высказывать свою идею в комментарии.

Окончательно

Спасибо, что прочитали. В приведенном выше коде используется TypeORM, и, конечно же, вы можете применить аналогичный механизм к другим ORM. Тем не менее, необработанный SQL-запрос более эффективен, когда речь идет о соединенных таблицах.

Наконец, я планирую поделиться простыми, но действенными советами. Я с нетерпением ждуваших подписчиков. И, пожалуйста, не стесняйтесь высказать свое мнение в комментарии.