Перевод статьи Люка Кауда и Мэтью Бирнома:
Мы отправились в путь, чтобы разделить наш монолит PHP на микросервисную архитектуру, причем большинство новых сервисов написано в Go. В этот период наша внешняя команда также приняла безопасность типов, перейдя от Javascript к TypeScript & React.
Имея безопасность типов в нашем бэкэнд и интерфейсе стало очевидно, что наши заказные конечные точки REST не смогли преодолеть разрыв типа. Нам нужен был способ объединить эти системы типов и распутать наши конечные точки API. Но нам нужна была безопасная API система. и GraphQL выглядел многообещающим. Однако, как мы это выяснили, мы поняли, что там не было серверного подхода, который отвечал всем нашим потребностям. Итак, мы разработали свои собственные, которые мы называем gqlgen.
Содержание статьи
Что такое GraphQL?
—
GraphQL — это язык запросов для API, который дает полное и понятное описание данных и дает клиентам возможность запросить именно то, что им нужно (и не получить ничего постороннего).
Например, мы можем определить типы: скажем, Пользователь имеет некоторые поля, в основном такие скаляры, как имя и высота но также и другие сложные типы, такие как место .
В отличие от REST, мы запрашиваем конечную точку GraphQL описывая форму результата :
{ user (id: 10) { название, местоположение {lat, long} } }
Поля могут принимать аргументы, которые работают аналогично параметрам запроса, и они могут быть на любом уровне графика.
Из приведенного выше запроса сервер возвращает:
{ «пользователь»: { «имя»: «Боб», "место нахождения": { "lat": 123.456789, "lon": 123.456789 } } }
Это мощно, потому что это дает нам систему общего типа, которую могут понять как клиент, так и сервер, а также дает нам удивительную возможность повторного использования. Что делать, если мы хотели построить наши 3 места лучших друзей на другом экране?
{ user (id: 10) { друзья (ограничение: 3) { название, местоположение {lat, long} } } }
и мы вернемся
{ «пользователь»: { «друзья»: [ { "name": "Carol", "location": {"lat": 1, "lon": 1} }, { "name": "Carlos", "location": {"lat": 2, "lon": 2} }, { "name": "Charlie", "location": {"lat": 3, "lon": 3} }, ] } }
Прощай, оговоренные конечные точки, приветливые, понятные, совместимые API!
Если Вам нужно купить сервер — переходите по ссылке, где вы найдете выделенный или VPS/VDS сервер на любой вкус.
Как gqlgen сравнивается с другими подходами к серверу GraphQL?
—
Первое, что вам нужно сделать, когда вы решите использовать GraphQL, — это решить, какую библиотеку серверов использовать. Оказывается, существует несколько различных подходов к определению типов и выполнения запросов основных ролей нашего сервера GraphQL.
Определение типов
Первое, что нам нужно сделать для любого сервера GraphQL, — это определить типы. Это позволяет серверу проверять входящие запросы и предоставлять API интроспекции, которые могут включать автозаполнение и другие полезные функции. Существует три основных подхода к определению типов:
1. Пользовательский язык, специфичный для конкретного домена
Вы можете создать дерево типов непосредственно на выбранном вами языке программирования. Это проще всего реализовать для серверной библиотеки, но часто приводит к большому количеству кода для записи пользователем. DSL отлично работают на некоторых языках, но в Go они очень многословны:
var queryType = graphql.NewObject (graphql.ObjectConfig { Имя: «Запрос», Поля: graphql.Fields { "short": & graphql.Field { Тип: briefType, Args: graphql.FieldConfigArgument { "id": & graphql.ArgumentConfig { Тип: graphql.NewNonNull (graphql.String), }, }, }, }, })
Эталонная реализация graphql-js использует этот подход, и многие реализации сервера последовали этому примеру, что делает этот подход наиболее распространенным. Будучи полностью динамичным, вы можете определить схему «на лету» на основе динамического ввода. Это не общее требование, но если вам это нужно, это единственный способ.
Недостатки
- Потеря безопасности типа (время компиляции): интенсивное использование открытого интерфейса {} и отражение.
- Смешивание декларативного кода определения схемы с императивным кодом распознавателя, что затрудняет вложение инъекций.
- Код определения схемы невероятно подробен по сравнению с целевым языком определения схемы.
Этот подход, как правило, очень утомительный и подверженный ошибкам, и оставляет вас с чем-то, что не особенно читаемо. Это становится еще хуже, когда на графиках есть петли.
Используется graphql-go / graphql
2. Схема первая
Сравните приведенную выше DSL с эквивалентным языком определения схемы (SDL):
введите Query { Краткое (id: String!): Краткая }
Краткая, краткая и легко читаемая. Это также языковой агностик, поэтому ваша команда разработчиков может использовать mocks, сгенерированные из SDL, для быстрого развертывания сервера, который отвечает на запросы и начинает создавать клиентский код одновременно с сервером.
Используется 99designs / gqlgen и graph-gophers / graphql.
3. Отражение
Этот подход предполагает наименьшую работу, так как нам не нужно явно объявлять типы GraphQL. Вместо этого мы можем отображать типы с нашего языка и строить сервер GraphQL.
Отражение звучит неплохо на бумаге, но если вы хотите использовать всю гамму функций GraphQL, вам нужно использовать язык, который очень близко относится к GraphQL. Автоматически создавать интерфейсы и союзы на языке утиного языка сложно.
. Говоря об этом, отражение используется в библиотеке graphql-ruby:
Типы классов :: ProfileType> Типы :: BaseObject field: id, ID, null: false field: name, String, null: false field: avatar, Types :: PhotoType, null: true конец
Хотя это может хорошо работать для таких языков, как Ruby (где DSL являются обычным явлением), система ограничения Go ограничивает возможности этого подхода.
Используется samsarahq / thunder
Выполнение запросов
Теперь, когда мы знаем, что мы разоблачаем, теперь нам нужно написать код для ответа на эти запросы GraphQL. Каждый шаг этапа выполнения GraphQL требует вызова функции, которая выглядит примерно так:
Выполнить ('Query.brief', short, {id: "123"}) -> Краткое описание
Опять же, есть несколько подходов к выполнению этих запросов:
1. Выставить общую подпись функции
Самый прямой подход заключается в том, чтобы разоблачить общую подпись функции непосредственно пользователю и позволить им обрабатывать все.
var queryType = graphql.NewObject (graphql.ObjectConfig { Имя: «Запрос», Поля: graphql.Fields { "short": & graphql.Field { // другие реквизиты здесь, но не важны прямо сейчас Resolve: func (p graphql.ResolveParams) (интерфейс {}, ошибка) { return mydb.FindBriefById (p.Args ["id"]. (строка)) }, }, }, })
Здесь есть несколько вопросов:
- Нам нужно иметь дело с распаковкой самих аргументов с карты [string] интерфейса {}
- id может не быть строкой
- Правильный ли тип возврата?
- Даже если это правильный тип, у него есть правильные поля?
- Как мне установить зависимости, такие как соединение с базой данных?
Библиотека может проверять результат во время выполнения, а обширное модульное тестирование может уловить эти проблемы.
Опять же, мы можем объявлять новые преобразователи и типы во время выполнения без перекомпиляции. Если вам нужна эта функция, вам, вероятно, нужен такой подход.
Используется graphql-go / graphql и graphql-js.
2. Измерение времени выполнения для типов
Мы можем позволить пользователю определить сами функции с типами, которые они ожидают, и использовать отражение, чтобы проверить правильность всего.
type query struct { db * myDb } func (q * query) Краткая (строка id) BriefResolver { return briefResolver {db, q.db.FindBriefById (id)} } type briefResolver struct { db * myDb * db.Brief } func (b * shortResolver) ID () строка {return b.ID} func (b * briefResolver) State () string {return b.State} func (b * briefResolver) UserID () string {return b.UserID}
Это читает немного лучше, библиотека выполнила всю процедуру распаковки для нас, и мы можем вводить зависимости. Но дочерние резольверы должны иметь свои зависимости, вводимые вручную. Кроме того, не существует безопасности времени компиляции, поэтому эта работа падает на проверки времени выполнения. По крайней мере, на этот раз мы можем статически проверить весь график при загрузке, а не на 100% -ный охват кода, чтобы поймать проблемы.
Используется графа-сусликами / graphql-go и samsarahq / thunder
Здание gqlgen
—
. Когда мы исследовали GraphQL, мы попытались использовать как графические, так и графические-gophers / graphql-go в различных проектах. Мы обнаружили, что граф-суслики / graphql-go имеют лучшую систему типов, но это не полностью отвечает нашим потребностям. Мы решили попробовать и постепенно сделать его более удобным.
Начиная с Go 1.4, была начальная поддержка для генерации кода через go generate, но ни один из существующих серверов GraphQL не воспользовался этим. Мы поняли, что вместо выполнения проверок времени выполнения мы можем генерировать интерфейсы для резольверов, и компилятор мог проверить, что все было выполнено правильно.
// в сгенерированном коде тип интерфейса QueryResolver { Краткая (ctx context.Context, строка id) (* Краткая информация, ошибка) } type Brief struct { Строка идентификатора Строка состояния UserID int }
// в нашем коде type queryResolver struct { db * myDb } func (r * queryResolver) Краткая (ctx context.Context, строка id) (* Краткая информация, ошибка) { b, err: = q.db.FindBriefById (id) если err! = nil { return nil, err } return Кратко { ID: b.ID, Состояние: b.State, UserID: b.UserID, }, ноль }
Отлично! Теперь наш компилятор мог рассказать нам, когда наши сигнатуры распознавателя не соответствовали нашей схеме GraphQL. Мы также переключились на более MVC-подобный подход, где граф резольвера является статическим, а зависимости могут быть введены один раз во время загрузки, а затем должны быть введены в каждый узел.
Привязка к моделям
Даже после создания типов безопасных интерфейсов распознавателя мы все еще писали немного кода ручного картографа. Что, если мы позволим генератору кода проверить нашу существующую модель базы данных, чтобы увидеть, соответствует ли она схеме GraphQL? Если бы это было так, мы могли бы использовать этот тип непосредственно в сигнатурах распознавателя.
// в сгенерированном коде тип интерфейса QueryResolver { Краткая (ctx context.Context, строка id) (* db.Brief, ошибка) }
// в нашем коде type queryResolver struct { db * myDb } func (r * queryResolver) Краткая (ctx context.Context, строка id) (* db.Brief, ошибка) { return q.db.FindBriefById (id) }
Совершенный. Теперь наш код резольвера был действительно безопасным клеем типа! Это отлично подходит для демонстрации моделей баз данных или даже хорошо типизированных клиентов API (protobuf, Thrift) над GraphQL.
Но что должно произойти с полями, которые не существуют в модели базы данных? Давайте создадим еще один резольвер.
// в сгенерированном коде тип Интерфейс BriefResolver { Владелец (ctx context.Context, * db.Brief) (* db.User, ошибка) }
// в нашем коде type briefResolver struct { db * myDb } func (r * briefResolver) Владелец (ctx context.Context, short * db.Brief) (* db.User, error) { return q.db.FindUserById (short.OwnerID) }
Создать код маршаллинга и выполнения
Мы не писали почти никаких шаблонов и не имели полной безопасности в наших решениях! Но большая часть фазы выполнения по-прежнему использует исходную систему отражения от графов-сусликов, и отражение никогда не бывает ясным. Давайте заменим логику разбора аргументов на основе рефлекса и логику вызова resolver с сгенерированным кодом:
func (ec * executeContext) _Brief (ctx context.Context, sel ast.SelectionSet, obj * model.Brief) graphql.Marshaler { поля: = graphql.CollectFields (ctx, sel, briefImplementors) out: = graphql.NewOrderedMap (len (поля)) для i, field: = поля диапазона { out.Keys [i] = field.Alias switch field.Name { case "__typename": out.Values [i] = graphql.MarshalString ("Brief") Идентификатор дела": out.Values [i] = graphql.MarshalString (obj.ID) case "state": out.Values [i] = graphql.MarshalString (obj.State) case "user": out.Values [i] = _MarshalUser (ec.resolvers.User.Owner (ctx, obj)) } } возвращаться }
* Примечание: Это упрощенный пример сгенерированного кода из gqlgen 0.5.1. Реальный код обрабатывает одновременное выполнение и барботирование ошибок.
Мы можем статически генерировать все выбор полей, привязки и сортировки json. Нам не нужна ни одна линия отражения для выполнения запроса GraphQL! Теперь компилятор может поймать ошибки на всем пути для нас. Он может видеть каждую кодировку через время выполнения и улавливать большинство ошибок. Мы получаем отличные трассировки стека, когда что-то ломается, и это позволяет быстро и быстро выполнять функции внутри gqlgen и в наших приложениях.
Примерно в этот момент мы конвертировали одно из наших приложений для разработки из графика, используя PR:
- удалено 600 строк написанных вручную, трудночитаемых, подверженных ошибкам DSL
- добавлено 70 строк схемы
- добавлено 70 строк типа безопасного кода распознавателя
- добавлено 1000 строк сгенерированного кода
Принять участие
—
Ускорить вперед 6 месяцев, и мы видели 619 коммиттов из 31 разных участников в gqlgen, что делает его одним из большинство полнофункциональных библиотек GraphQL для Go. У нас был gqlgen в производстве на 99designs в течение большей части этого года, и мы видели действительно положительный ответ от сообщества Go / GraphQL.
Это только начало! Некоторые из скоростных функций скоро включают:
- Лучшая поддержка директив через плагиновую систему — возможность комментировать схему с помощью валидации и создания плагинов, которые обеспечивают бесшовную интеграцию с ORM с ORE, такими как Prisma или XO.
- Сшивание схемы — объединение нескольких серверов GraphQL, чтобы разоблачить единый, согласованный общий вид.
- Связывание gRPC / Twirp / Thrift на основе схемы — возможность привязывать внешние сервисы к вашему графику так же просто, как @grpc (служба: «http: // service», метод: «Foobar»)
Мы считаем, что gqlgen — лучший способ построить сервер GraphQL в Go и, возможно, даже на любом языке. До сих пор мы отправили множество функций, и многие другие приходят и надеются, что вы присоединитесь к нам в GitHub или Gitter и присоединитесь к приключениям.
—
Этот пост был написан в сотрудничестве с Люком Каудом и Мэтью Бирном.