В 99designs мы находимся в процессе деконструкции нашего монолита PHP в архитектуру микросервисов, при этом большинство новых сервисов написано на Go. За это время наша фронтенд-команда также внедрила типобезопасность, перейдя с Javascript на TypeScript и React. .

Логотип gqlgen от V'Official

После реализации безопасности типов во внутренней и внешней части стало ясно, что наши пользовательские конечные точки REST не могут устранить разрыв между типами. Нам нужен был способ объединить эти системы типов и распутать конечные точки нашего API.

Нам нужна была безопасность типов для API. GraphQL выглядел многообещающе. Однако, изучив его, мы поняли, что не существует серверного подхода, который отвечал бы всем нашим требованиям. Поэтому мы разработали свой собственный, который назвали gqlgen.

Что такое GraphQL?

GraphQL — это язык запросов для API, который обеспечивает полное и четкое описание данных и позволяет клиентам запрашивать именно то, что им нужно (не получая при этом ничего ненужного).

Например, мы можем определить типы: скажем, пользователь (пользователь) есть несколько полей, в основном скалярных, таких как имя и высота (имя & высота), но и другие сложные типы, такие как местоположение (расположение).

В отличие от REST, мы запрашиваем конечную точку GraphQL. описание формы результата:

{
  user(id: 10) {
    name
    location {
      lat
      long
    }
  }
}

Поля могут принимать аргументы, которые действуют так же, как параметры запроса, и могут находиться на любом уровне графа.

По приведенному выше запросу сервер возвращает:

{
  "user": {
    "name": "Bob",
    "location": {
      "lat": 123.456789,
      "lon": 123.456789
    }
  }
}

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

{
  user(id: 10) {
    friends(limit: 3) {
      name
      location {
        lat
        long
      }
    }
  }
}

и мы получаем:

{
  "user": {
    "friends": [
      {
        "name": "Carol",
        "location": { "lat": 1, "lon": 1 }
      },
      {
        "name": "Carlos",
        "location": { "lat": 2, "lon": 2 }
      },
      {
        "name": "Charlie",
        "location": { "lat": 3, "lon": 3 }
      }
    ]
  }
}

Прощайте, пользовательские конечные точки, здравствуйте, безопасные, открытые и согласованные API!

Чем gqlgen отличается от других подходов к созданию серверов GraphQL?

Первое, что вам нужно сделать при принятии решения об использовании GraphQL, — это решить, какую серверную библиотеку использовать. Оказывается, существует несколько разных подходов к определение типа И отвечать на запросы — основные функции нашего сервера GraphQL.

Определение типов

Первое, что нам нужно сделать для любого сервера GraphQL, — это определить типы. Это позволит серверу проверять входящие запросы и предоставлять API самоанализа, который может включить автозаполнение и другие полезные функции. Существует три основных подхода к определению типов:

1. Язык, специфичный для предметной области

Вы можете создать дерево типов непосредственно на выбранном вами языке программирования. Это самый простой метод реализации для серверной библиотеки, но он часто требует от пользователя написания большого количества кода. DSL очень хорошо работают в некоторых языках, но в Go они очень многословны:

var queryType = graphql.NewObject(graphql.ObjectConfig{
  Name: "Query",
  Fields: graphql.Fields{
    "brief": &graphql.Field{
      Type: briefType,
      Args: graphql.FieldConfigArgument{
        "id": &graphql.ArgumentConfig{
          Type: graphql.NewNonNull(graphql.String),
        },
      },
    },
  },
})

Эталонная реализация графql-js использует этот подход, и многие реализации на стороне сервера последовали его примеру, что сделало его наиболее распространенным подходом. Полностью динамический подход означает, что вы можете определить схему «на лету» на основе динамических данных. Это не самое распространенное требование, но если оно вам нужно, это единственный способ.

ЧИТАТЬ   Один аккаунт PS5 на двоих | Купить игры для PS5 на двоих | Как поделиться игрой PSN с другом

По умолчанию

  • Потеря безопасности типов (во время компиляции): интенсивное использование средств общественной безопасности. interface{} и отражение.

  • Смешение кода определения декларативной схемы с кодом императивного разрешения, что затрудняет внедрение зависимостей.

  • Код определения схемы невероятно многословен по сравнению с собственным языком определения схемы.

Этот подход обычно очень утомителен и подвержен ошибкам, и в итоге вы получаете что-то не очень читабельное. Ситуация становится еще хуже, когда на графиках есть циклы.

Используется в Graphql-go/graphql

2. Сначала схема

Сравните приведенный выше DSL с эквивалентным языком определения схемы (SDL):

type Query {
  brief(id: String!): Brief
}

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

Используется в 99дизайнов/gqlgen, призма/graphqlgen И граф-суслики/graphql.

3. Размышления

Этот подход требует наименьших усилий, поскольку нам не нужно явно объявлять типы GraphQL. Вместо этого мы можем отразить типы нашего языка и построить на их основе сервер GraphQL.

На бумаге Reflection выглядит хорошо, но если вы хотите использовать весь спектр возможностей GraphQL, вам нужно использовать язык, который очень близко соответствует GraphQL. Автоматически создавать интерфейсы и объединения на языке с утиной типизацией очень сложно.

Однако в библиотеке Graphql-Ruby отражение используется с умом:

class Types::ProfileType > Types::BaseObject
  field :id, ID, null: false
  field :name, String, null: false
  field :avatar, Types::PhotoType, null: true
end

Хотя это может хорошо работать для таких языков, как Ruby (где распространен DSL), система ограничительных типов Go ограничивает возможности этого подхода.

Используется в самсарахк/гром

Запуск запросов

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

Execute('Query.brief', brief, {id: "123"}) -> Brief

Опять же, существует несколько подходов к выполнению этих запросов:

1. Определить общую сигнатуру функции

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

var queryType = graphql.NewObject(graphql.ObjectConfig{
  Name: "Query",
  Fields: graphql.Fields{
    "brief": &graphql.Field{
      // other props are here but not important right now

      Resolve: func(p graphql.ResolveParams) (interface{}, error) {
        return mydb.FindBriefById(p.Args["id"].(string))
      },
    },
  },
})

Здесь есть несколько проблем:

  • Надо разобраться с распаковкой. args С map[string]interface{}

  • id возможно, это не строка.

  • Это правильный тип обратной связи?

  • Даже если это правильный тип, есть ли у него правильные поля?

  • Как внедрить зависимости, такие как соединение с базой данных?

ЧИТАТЬ   Военнослужащие Пермского края награждены государственными наградами за участие в спецоперации

Библиотека может проверить результат во время выполнения, и тщательное модульное тестирование выявит эти проблемы.

Опять же, мы можем объявлять новые преобразователи и типы во время выполнения без перекомпиляции. Если вам нужны возможности такого типа, возможно, вам нужен именно такой подход.

Используется в Graphql-go/graphql И графql-js.

2. Отражение типов во время выполнения

Мы можем позволить пользователю определять функции ожидаемых типов и использовать отражение, чтобы проверить, что все правильно.

type query struct{
    db *myDb
}

func (q *query) Brief(id string) BriefResolver {
    return briefResolver{db, q.db.FindBriefById(id)}
}

type briefResolver struct {
    db *myDb
    *db.Brief
}

func (b *briefResolver) ID() string { return b.ID }
func (b *briefResolver) State() string { return b.State }
func (b *briefResolver) UserID() string { return b.UserID }

Это читается немного лучше: библиотека выполнила за нас всю логику распаковки, и мы можем внедрить зависимости. Но дочерние преобразователи должны внедрять свои зависимости вручную. Также отсутствует безопасность во время компиляции, поэтому эта работа остается на усмотрение проверок во время выполнения. По крайней мере, на этот раз мы можем статически проверять весь граф при загрузке, а не требовать 100% покрытия кода для обнаружения проблем.

Используется в граф-суслики/graphql-go И самсарахк/гром

здание gqlgen

Изучая GraphQL, мы попробовали и graphql-go/graphqlя graph-gophers/graphql-go в различных проектах. Мы нашли это graph-gophers/graphql-go имеет лучшую систему ввода, но она не полностью удовлетворяет нашим потребностям. Мы решили попробовать постепенно сделать его более практичным.

Генерация интерфейсов разрешения

Начиная с Go 1.4, появилась первоклассная поддержка генерации кода с использованием go generate, но ни один из существующих серверов GraphQL этим не воспользовался. Мы поняли, что вместо проверок во время выполнения мы можем генерировать интерфейсы для резолверов, и компилятор будет проверять, что всё реализовано правильно.

3ff9df313c28caab7bced9139f4daa57
// in generated code
type QueryResolver interface {
  Brief(ctx context.Context, id string) (*Brief, error)
}

type Brief struct {
  ID string
  State string
  UserID int
}
// in our code
type queryResolver struct{
    db *myDb
}

func (r *queryResolver) Brief(ctx context.Context, id string) (*Brief, error) {
  b, err :=  q.db.FindBriefById(id)
  if err != nil {
    return nil, err
  }
  return Brief {
    ID: b.ID,
    State: b.State,
    UserID: b.UserID,
  }, nil
}

Большой! Наш компилятор теперь мог сообщать нам, когда сигнатуры преобразователя не соответствуют нашей схеме GraphQL. Мы также перешли к подходу, более похожему на MVC, где граф преобразователя является статическим, а зависимости можно вводить один раз при запуске, а не вводить в каждый узел.

Ссылки на шаблоны

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

8b5dd83a8c794b0fb2889d2120520373
// in generated code
type QueryResolver interface {
  Brief(ctx context.Context, id string) (*db.Brief, error)
}
// in our code
type queryResolver struct{
  db *myDb
}

func (r *queryResolver) Brief(ctx context.Context, id string) (*db.Brief, error) {
  return q.db.FindBriefById(id)
}

Большой. Теперь наш код разрешения был просто надежным клеем! Это идеально подходит для предоставления моделей баз данных или даже хорошо типизированных клиентов API (protobuf, Thrift) через GraphQL.

ЧИТАТЬ   «Наша склонность — склонность отвергать жизнь, тупое ожидание конца». Режиссер Сергей Урсуляк - о добрых героях и мире, в котором мы живем

Но что должно произойти с полями, которых нет в модели базы данных? Давайте сгенерируем еще один преобразователь.

// in generated code
type BriefResolver interface {
  Owner(ctx context.Context, *db.Brief) (*db.User, error)
}
// in our code
type briefResolver struct{
  db *myDb
}

func (r *briefResolver) Owner(ctx context.Context, brief *db.Brief) (*db.User, error) {
  return q.db.FindUserById(brief.OwnerID)
}

Генерация кода маршалинга и выполнения

Мы почти не написали шаблонов и в наших преобразователях реализована полная типобезопасность! Но большая часть этапа исполнения по-прежнему использует исходную систему мышления graph-gophersА отражение никогда не бывает ясным. Давайте заменим логику распаковки аргументов и вызова преобразователя на основе отражения сгенерированным кодом:

89ff333601512afefe9f8979c6e24af8
func (ec *executionContext) _Brief(ctx context.Context, sel ast.SelectionSet, obj *model.Brief) graphql.Marshaler {
	fields := graphql.CollectFields(ctx, sel, briefImplementors)

	out := graphql.NewOrderedMap(len(fields))
	for i, field := range fields {
		out.Keys[i] = field.Alias

		switch field.Name {
		case "__typename":
			out.Values[i] = graphql.MarshalString("Brief")
		case "id":
			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))
		}
	}
	return out
}

Примечание. Это упрощенный пример кода, созданного из gqlgen 0.5.1. Реальный код реализует параллельное выполнение и разделение ошибок.

Мы можем статически генерировать весь выбор полей, привязку и маршалинг JSON. Нам не нужна ни одна строка размышления для выполнения запроса GraphQL! Компилятор теперь может обнаруживать ошибки на этом пути. Он может видеть каждый путь кода во время выполнения и обнаруживать большинство ошибок. В случае сбоя мы получаем отличную трассировку стека, что позволяет нам быстро перерабатывать функциональность gqlgen и наших приложений.

На данный момент мы перенесли одно из наших приложений разработки из graphql-goРП:

  • удаление 600 трудночитаемых и подверженных ошибкам строк рукописного DSL

  • добавлено 70 строк схемы

  • добавлено 70 строк типобезопасного кода разрешения.

  • добавлено 1000 строк сгенерированного кода

Участвовать

Отрывок из выступления Кристофера Бискардиса Going GraphQL на Gopherpalooza 2018

Отрывок из выступления Кристофера Бискардиса Going GraphQL на Gopherpalooza 2018

Прошло 6 месяцев, и мы увидели 619 коммитов в gqlgen от 31 участника, что сделало ее одной из самых многофункциональных библиотек GraphQL для Go. Большую часть этого года gqlgen находился в разработке на 99designs, и мы увидели очень положительный результат. ответ сообщества Go/GraphQL.

Это только началось! Скоро будут доступны следующие важные функции:

  • Улучшенная поддержка директив через систему плагинов — возможность аннотировать схему с проверкой и создавать плагины, обеспечивающие простую интеграцию с ORM на основе Codegen, такими как призма Или XO.

  • Сборка схемы: объединение нескольких серверов GraphQL для создания единого согласованного представления для всей вашей организации.

  • Привязка gRPC/Twirp/Thrift на основе схемы – возможность привязать внешние сервисы к вашему графу так же проста, как @grpc(service: " method: "Foobar").

Мы считаем, что gqlgen — лучший способ создать сервер GraphQL на Go и, возможно, даже на любом другом языке. На данный момент мы уже добавили много функций, но их будет еще больше, и мы надеемся, что вы присоединитесь к нам. GitHub Или Сетка и присоединиться к приключению.

Что еще почитать/посмотреть по теме

Source

От admin