Мы все ежедневно работаем с REST API. В REST при выполнении запроса мы отправляем ответ с полной полезной нагрузкой данных объекта. Звучит довольно просто, но у этого подхода есть два существенных недостатка. Допустим, API обслуживает разных клиентов, таких как App/Web, и им нужны разные типы данных (в зависимости от различий в дизайне App и Web). Таким образом, если бэкенд-разработчик в конечном итоге создаст общую конечную точку для обоих этих клиентов, мы можем либо недополучить фактические данные, которые нам нужны, либо получить слишком много данных, в то время как в идеале нам нужен только небольшой объем данных.

Теперь, чтобы решить эту проблему, вы уверены, что мы должны определить две разные конечные точки на серверной части? Один обслуживает приложение, а другой обслуживает Интернет.

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

Если вы хотите узнать больше о приведенной выше постановке проблемы, вы можете прочитать ее здесь: Backend For Frontend

Как насчет того, чтобы вместо нескольких API у нас был один GraphQL API. У него будет единственная точка входа; данные запрашиваются и извлекаются путем простой передачи набора обязательных полей в самой полезной нагрузке запроса. Таким образом, клиенты (приложение или Интернет) могут определить точный набор значений, которые они хотят, чтобы внутренний сервер извлекал/вычислял и отправлял их взамен. Facebook разработал его в 2012 году, а затем публично выпустил в 2015 году.

** Краткий отказ от ответственности. Этот учебник очень практичен и требует от вас изучения GraphQL 👩🏼‍💻

В этом блоге мы будем использовать библиотеки Golang и gqlgen для создания простого GraphQL API. Прежде чем углубиться, мы немного проверим Golang и Graphql.

Go (также называемый языком Golang или Go) — это скомпилированный и статически типизированный язык программирования с открытым исходным кодом, поддерживаемый Google. Он создан, чтобы быть простым, высокопроизводительным, удобочитаемым и эффективным.

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

Если вы новичок в Golang, вы можете начать с Golang tour, чтобы изучить основы языка.

Что касается GraphQL, давайте разберемся с некоторыми основами, прежде чем мы начнем:

  1. GraphQL создает интерфейс уровня данных вашего приложения и служит API для клиентов вашего сервиса.
  2. Схема GraphQL может быть определена тремя способами:
    — с помощью объекта GraphQLSchema
    — с помощью запроса самоанализа
    — записать схему в файл .graphql с помощью SDL (язык определения схемы).
  3. Вы можете сделать четыре основных запроса в GraphQL: запрос, мутация, фрагмент и подписка.
  4. Вместо того, чтобы делать запросы с разными HTTP-глаголами к другим URL-адресам, все запросы выполняются как POST запросы к одному URL-адресу. В запросе POST в теле отправляется строка, указывающая, какой запрос или мутацию следует выполнить, и какие свойства ожидаются в ответ.

Пример запроса/ответа GraphQL будет выглядеть так:

Давайте сами создадим образец API, чтобы полностью понять, как он работает 🤝

1. Инициализировать проект

Создайте новый каталог и инициализируйте в нем модули go.
Я создал новый каталог как go_graphql

mkdir go_graphql
cd go_graphql
go mod init go_graphql

2. Установите зависимости

Добавьте github.com/99designs/gqlgen к вашему project's tool.go. Он установит зависимости для этого руководства.

printf '// +build tools\npackage tools\nimport _ "github.com/99designs/gqlgen"' | gofmt > tools.go
go mod tidy

3. Инициализировать gqlgen

Теперь используйте команду ‍‍gqlgen init для настройки проекта gqlgen. Он инициализирует конфигурацию gqlgen, сгенерирует модели и создаст скелет проекта для GraphQL.

go run github.com/99designs/gqlgen init

Теперь структура вашего проекта будет выглядеть примерно так:

├── go.mod
├── go.sum
├── gqlgen.yml               
├── graph
│   ├── generated            
│   │   └── generated.go
│   ├── model                
│   │   └── models_gen.go
│   ├── resolver.go          
│   ├── schema.graphqls     
│   └── schema.resolvers.go 
└── server.go             

Вот описание сгенерированных файлов от gqlgen:

  • gqlgen.yml — Конфиг-файл gqlgen, ручки для управления сгенерированным кодом.
  • graph/generated/generated.go — среда выполнения GraphQL, основная часть сгенерированного кода.
  • graph/model/models_gen.go — Сгенерированные модели, необходимые для построения графика. При необходимости мы заменим модели нашими моделями.
  • graph/schema.graphqls — это файл, в который вы будете добавлять схемы GraphQL.
  • resolver.go — Тип преобразователя корневого графа. Этот файл не будет восстановлен.
  • graph/schema.resolvers.go — здесь живет код вашего приложения. generated.go вызовет это, чтобы получить данные, запрошенные пользователем.
  • server.go — точка входа в ваше приложение. Он устанавливает http.Handler для сгенерированного сервера GraphQL. Настройте его так, как считаете нужным.

Всякий раз, когда мы меняем схему, нам нужно повторно запускать команду generate.

go run github.com/99designs/gqlgen generate

Запустите сервер с go run server.go и откройте браузер, и вы должны увидеть игровую площадку GraphQL, чтобы убедиться, что настройка выполнена правильно.

Площадка GraphQL будет выглядеть примерно так:

4. Определите нашу схему

Чтобы GraphQL понимал структуру наших объектов, нам нужно определить наши объекты в файле схемы. По умолчанию поле models в gqlgen.yml указывает на models_gen.go, которое вы должны заменить своим объектом. Файл graph/schema.graphqls содержит схему по умолчанию. Либо вы можете добавить новые файлы схемы в папку графа и указать их в gqlgen.yml, либо обновить только этот файл.

Схема по умолчанию:

# GraphQL schema example
#
# https://gqlgen.com/getting-started/

type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User!
}

type User {
  id: ID!
  name: String!
}

type Query {
  todos: [Todo!]!
}

input NewTodo {
  text: String!
  userId: String!
}

type Mutation {
  createTodo(input: NewTodo!): Todo!
}

Некоторые ключевые моменты, которые следует отметить здесь:

  • Структурное имя после type называется Character. Каждый Character имеет свой собственный field(s) и каждый field имеет свой тип.
  • ! в конце типа означает, что поле является необнуляемым.
  • input типов, которые используются в качестве аргументов для ваших преобразователей.
  • Все преобразователи делятся на две категории:
    * query: используется для получения информации, эквивалентно GET маршрутам REST API
    * mutations: используется для создания, обновления и удаления данных, эквивалентно REST, POST и PUT.

Все возможные запросы объявлены в типе Query, а мутации в типе Mutation в файле выше. Чтобы предоставить входные данные преобразователю createTodo в виде аргумента с именем input, он должен соответствовать типу ввода NewTodo.

Если мы хотим сравнить команды операций CRUD в SQL и GraphQL:

При этом GraphQL является самодокументируемым, зная, какие функции запускать при поступлении различных запросов. Однако нам нужно определить функции распознавателя, чтобы они соответствовали объявлениям распознавателя, которые будут найдены в schema.resolvers.go. Перед этим давайте сначала посмотрим пример Query в GraphQl.

Простой запрос

Давайте пока запустим простой запрос и вернем жестко закодированные данные.

Откройте файл schema.resolvers.go и проверьте функцию Todos:

func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {

Здесь он принимает Context в качестве входных данных и возвращает часть Todo и ошибку, если она присутствует. Аргумент ctx содержит данные от лица, отправившего запрос, например, какой пользователь работает с приложением и т. д.

Давайте пока отправим фиктивный ответ для этой функции в schema.resolvers.go:

func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
   var todos []*model.Todo
   dummyTodo := model.Todo{
      Text: "our dummy text",
      Done: true,
      User: &model.User{Name: "testing123"},
   }
   todos = append(todos, &dummyTodo)
   return todos, nil
}

Теперь запустите сервер с go run server.go и введите следующий запрос на игровой площадке Graphql:

query {
 todos{
    text
    done,
    user{
      name
    }
  }
}

Ответ будет выглядеть примерно так:

{
  "data": {
    "todos": [
      {
        "text": "our dummy text",
        "done": true,
        "user": {
          "name": "testing123"
        }
      }
    ]
  }
}

Мутация

Давайте реализуем мутацию CreateTodo. Поскольку в этом руководстве у нас нет базы данных, мы получим данные задачи, создадим объект задачи, добавим его в список и отправим обратно для ответа!

Чтобы реализовать это, нам нужно внести изменения в файл resolver.go и schema.resolvers.go.

В файле resolver.go мы можем добавить свойства к структуре Resolver, которая становится доступной через экземпляр преобразователя, представленный r. Мы добавим свойство, представляющее собой массив элементов todo, который мы можем использовать для отслеживания созданных элементов Todo.

import "gqlgen_tutorial/graph/model"
// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.
type Resolver struct{
        TodosList []*model.Todo
}

Теперь давайте реализуем эти функции преобразователя в schema.resolvers.go:

package graph

// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.

import (
   "context"
   "fmt"
   "gqlgen_tutorial/graph/generated"
   "gqlgen_tutorial/graph/model"
)

// CreateTodo is the resolver for the createTodo field.
func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {

   // CREATE A NEW TODO
   todo := &model.Todo{
      ID:   fmt.Sprint(len(r.TodosList) + 1),
      Text: input.Text,
      Done: false,
      User: &model.User{
         ID:   input.UserID,
         Name: fmt.Sprintf("Elmo%s", fmt.Sprint(len(r.TodosList)+1)),
      },
   }
   // ADD THE TODO TO THE TODOS ARRAY
   r.TodosList = append(r.TodosList, todo)
   // RETURN ALL THE TODOS
   return todo, nil
}

// Todos is the resolver for the todos field.
func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {

   return r.TodosList, nil
}

// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }

// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }

type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }

Теперь запустите сервер с go run server.go и введите следующий запрос на игровой площадке GraphQL:

mutation {
  createTodo(input: {
    text: "Test Graphql 1"
    userId: "456"
  })
  {
    id
    text
    done
  }
}

И вы получите такой ответ:

{
  "data": {
    "createTodo": {
      "id": "1",
      "text": "Test Graphql 1",
      "done": false
    }
  }
}

Давайте реализуем методы RemoveTodo и FindTodo. Как упоминалось выше, нам нужно будет добавить для них Mutation, Query и input в файле schema.graphqls.

Обновлено schema.graphqls:

type Mutation {
  createTodo(input: NewTodo!): Todo!
  removeTodo(input: DeleteTodo!): Todo!
}

input DeleteTodo {
  todoId: String!
}
type Query {
  todos: [Todo!]!
  findTodo(input: FindTodo!): Todo!
}
input FindTodo {
  todoId: String!
}

Давайте повторно запустим команду генерации и увидим волшебство 🪄

go run github.com/99designs/gqlgen generate

Вы заметите:

1. В schema.resolvers.go добавлена ​​новая функция как

// RemoveTodo is the resolver for the removeTodo field.
func (r *mutationResolver) RemoveTodo(ctx context.Context, input model.DeleteTodo) (*model.Todo, error) {
   panic(fmt.Errorf("not implemented: RemoveTodo - removeTodo"))
}

2. В файле model/models_gen.go будет присутствовать newtype:

type DeleteTodo struct {
   TodoID string `json:"todoId"`
}
type FindTodo struct {
   TodoID string `json:"todoId"`
}

Теперь реализуйте функции, упомянутые выше:

// RemoveTodo is the resolver for the removeTodo field.
func (r *mutationResolver) RemoveTodo(ctx context.Context, input model.DeleteTodo) (*model.Todo, error) {
   index := -1
   for i, todos := range r.TodosList {
      if todos.ID == input.TodoID {
         index = i
      }
   }
   if index == -1 {
      return nil, errors.New("cannot find todo you are looking for")
   }
   todos := r.TodosList[index]
   r.TodosList = append(r.TodosList[:index], r.TodosList[index+1:]...)

   return todos, nil
}
// FindTodo is the resolver for the findTodo field.
func (r *queryResolver) FindTodo(ctx context.Context, input model.FindTodo) (*model.Todo, error) {
   for _, todos := range r.TodosList {
      if todos.ID == input.TodoID {
         return todos, nil
      }
   }
   return nil, errors.New("cannot find todo you are looking for")
}

Снова запустите сервер с go run server.go и попробуйте эти два преобразователя:

НайтиTodo

Запрос:

query {
 findTodo(input: {todoId: "1"}) {
    text
    done,
    user{
      name
    }
  }
}

Ответ:

{
  "data": {
    "findTodo": {
      "text": "Test Graphql 1",
      "done": false,
      "user": {
        "name": "Elmo1"
      }
    }
  }
}

УдалитьTodo

Запрос:

mutation {
  removeTodo(input: {
    todoId: "3"
  })
  {
    id
    text
    done
  }
}

Ответ:

{
  "data": {
    "removeTodo": {
      "id": "3",
      "text": "Test Graphql 3",
      "done": false
    }
  }
}

Теперь, если вы снова сделаете запрос, вы получите в ответ только две задачи:
Запрос:

query {
 todos{
    text
    done,
    user{
      name
    }
  }
}

Ответ:

{
  "data": {
    "todos": [
      {
        "text": "Test Graphql 1",
        "done": false,
        "user": {
          "name": "Elmo1"
        }
      },
      {
        "text": "Test Graphql 2",
        "done": false,
        "user": {
          "name": "Elmo2"
        }
      }
    ]
  }
}

В этом вы можете многому научиться. Gqlgen – рекомендуемый инструмент для новичков, которые хотят попробовать GraphQL в своих проектах. Он имеет приятную и простую функцию создания лесов и отличную документацию. Я добавляю другие ресурсы, которые помогут вам глубже погрузиться в это. Удачного обучения 🙂

Другие источники

Ссылки: