Должен ли GraphQL DataLoader переносить запрос к базе данных или переносить запросы к методам обслуживания?

У меня есть очень распространенная схема GraphQL, как это (псевдокод):

Post {
  commentsPage(skip: Int, limit: Int) {
    total: Int
    items: [Comment]
  }
}

Таким образом, чтобы избежать проблемы n+1 при запросе нескольких Post объекты я решил использовать Facebook Dataloader.

Поскольку я работаю над трехуровневым многоуровневым приложением Nest.JS (Resolver-Service-Repository), у меня возник вопрос:

я должен обернуть свои методы репозитория с DataLoader или я должен обернуть мои сервисные методы с Dataloder?

Ниже приведен пример моего метода обслуживания, который возвращает Comments страница (т.е. этот метод вызывается из commentsPage распознаватель собственности). Внутри сервиса я использую 2 репозитория (#count а также #find):

@Injectable()
export class CommentsService {
    constructor(
        private readonly repository: CommentsRepository,
    ) {}

    async getCommentsPage(postId, dataStart, dateEnd, skip, limit): PaginatedComments {
        const counts = await this.repository.getCount(postId, dateStart, dateEnd);
        const itemsDocs = await this.repository.find(postId, dateStart, dateEnd, skip, limit);
        const items = this.mapDbResultToGraphQlType(itemsDocs);
        return new PaginatedComments(total, items)
    }
}

Поэтому я должен создать отдельные экземпляры Dataloader для каждого из методов хранилища (#count, #find и т. д.) или я должен просто обернуть весь метод обслуживания с помощью Dataloader (commentsPage решатель свойств будет работать только с Dataloader, а не с сервисом)?

1 ответ

Отказ от ответственности: я не эксперт в Next.js, но я написал хорошую группу загрузчиков данных, а также работал с автоматически сгенерированными загрузчиками данных. Я надеюсь, что я могу дать немного понимания, тем не менее.

В чем собственно проблема?

Хотя ваш вопрос кажется относительно простым или вопрос, вероятно, гораздо сложнее, чем этот. Я думаю, что реальная проблема заключается в следующем: использовать шаблон загрузчика данных или нет для конкретного поля необходимо в зависимости от поля. Шаблон службы хранилища +, с другой стороны, пытается абстрагировать это решение, предоставляя абстрактные и мощные способы доступа к данным. Одним из выходов было бы просто "dataloaderify" каждый метод вашего обслуживания. К сожалению, на практике это нереально. Давайте рассмотрим почему!

Dataloader предназначен для поиска по значению ключа

Dataloader предоставляет кэш обещаний для сокращения повторных обращений к базе данных. Чтобы этот кеш работал, все запросы должны быть простыми поисками значения ключа (например, userByIdLoader, postsByUserIdLoader). Это быстро становится недостаточно, как в одном из ваших примеров ваш запрос к хранилищу имеет много параметров:

this.repository.find(postId, dateStart, dateEnd, skip, limit);

Конечно, технически вы могли бы сделать { postId, dateStart, dateEnd, skip, limit } ваш ключ, а затем каким-то образом хешировать содержимое для создания уникального ключа.

Написание запросов Dataloader на порядок сложнее, чем обычные запросы

Когда вы реализуете запрос на загрузчик данных, он внезапно должен работать со списком входных данных, необходимых для первоначального запроса. Вот простой пример SQL:

SELECT * FROM user WHERE id = ?
-- Datalaoded
SELECT * FROM user WHERE id IN ?

Хорошо, теперь пример хранилища сверху:

SELECT * FROM comment WHERE post_id = ? AND date < ? AND date > ? OFFSET ? LIMIT ?
-- Dataloaded
???

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

Хорошо, так в чем же решение?

Первое, что я понимаю, что делает Facebook - это очень точное сопоставление полей и методов обслуживания. Вы могли бы сделать то же самое. Таким образом, вы можете принять решение в сервисном методе, хотите ли вы использовать загрузчик данных или нет. Например, я не использую загрузчики данных в корневых запросах (например, { getPosts(filter: { createdBefore: "...", user: 234 }) { .. }) но в подполях типов, которые появляются в списках { getAllPosts { comments { ... } }, Корневой запрос не будет выполняться в цикле и поэтому не подвержен проблеме n+1.

Ваш репозиторий теперь показывает, что можно "эффективно запрашивать" (как в твиттере Ли), например поиск по внешнему / первичному ключу или фильтрацию поиска всех запросов. Служба может затем обернуть, например, поиск ключей в загрузчике данных. Часто я заканчиваю фильтрацией небольших списков в своей бизнес-логике. Я думаю, что это прекрасно для небольших приложений, но может быть проблематичным при масштабировании. Помощники GraphQL Relay для JavaScript делают нечто подобное, когда вы используете connectionFromArray функция. Разбиение на страницы не выполняется на уровне базы данных, и, вероятно, это нормально для 90% соединений.

Некоторые источники для рассмотрения

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