image


Всем привет! О GraphQL много статей на Хабре, но пробежавшись по ним обнаружил, что все они обходят стороной такой замечательный язык как Go. Сегодня попробую исправить это недоразумение. Для этого напишем API на Go с использованием GraphQL.


Если совсем коротко: GraphQL это язык запросов для построения API, который описывает в каком виде запрашивать и возвращать данные (более подробная информация на официальном ресурсе graphql.github.io и на хабре)


Поспорить о том, что лучше GraphQL или REST можно тут


У нас будет классическое API: CRUD (Create, Read, Update, Delete) добавление, получение, редактирование и удаление товаров в интернет магазине.
На стороне сервера будем использовать готовую реализацию GraphQL graphql-go


Для начала необходимо скачать graphql-go, это можно сделать командой


go get github.com/graphql-go/graphql

Далее, опишем структуру товара (в упрощенном виде)


type Product struct {
    ID       int64  `json:"id"`
    Name     string `json:"name"`
    Info     string `json:"info,omitempty"`
    Price    float64 `json:"price"`
}

ID — уникальный идентификатор, Name — название, Info — информация о товаре, Price — цена


Первое, что необходимо сделать, это вызвать метод Do, который в качестве входных параметров принимает схему данных и параметры запроса. А вернет нам результирующие данные (для дальнейшей передачи на клиент)


result := graphql.Do(graphql.Params{
  Schema:        schema,
  RequestString: query,
})

Полный код
func executeQuery(query string, schema graphql.Schema) *graphql.Result {
    result := graphql.Do(graphql.Params{
        Schema:        schema,
        RequestString: query,
    })
    if len(result.Errors) > 0 {
        fmt.Printf("errors: %v", result.Errors)
    }
    return result
}

func main() {
    http.HandleFunc("/product", func(w http.ResponseWriter, r *http.Request) {
        result := executeQuery(r.URL.Query().Get("query"), schema)
        json.NewEncoder(w).Encode(result)
    })
    http.ListenAndServe(":8080", nil)
}

Schema — схема данных, RequestString — значение параметра строки запроса, в нашем случае значение query


Schema (Схема)


Схема принимает два корневых типа данных: Query — неизменяемые данные, Mutation — изменяемые данные


var schema, _ = graphql.NewSchema(
    graphql.SchemaConfig{
        Query:    queryType,
        Mutation: mutationType,
    },
)

Query (Запросы)


Query служит для чтения (и только чтения) данных. С помощью Query мы указываем какие данные должен вернуть сервер.
Напишем реализацию типа данных Query, в нашем случае он будет содержать поля с получением информации о единичном товаре (product) и списке товаров (list)


var queryType = graphql.NewObject(
    graphql.ObjectConfig{
        Name: "Query",
        Fields: graphql.Fields{
            /* Получение продукта по ID
               http://localhost:8080/product?query={product(id:1){name,info,price}}
            */
            "product": &graphql.Field{
                Type:        productType,
                Description: "Get product by id",
                // Получаем список аргументов, для дальнейшего использования
                Args: graphql.FieldConfigArgument{
                    // В данном случае нам необходим только id
                    "id": &graphql.ArgumentConfig{
                        Type: graphql.Int,
                    },
                },
                Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                    id, ok := p.Args["id"].(int)
                    if ok {
                        // Поиск продукта с ID
                        for _, product := range products {
                            if int(product.ID) == id {
                                return product, nil
                            }
                        }
                    }
                    return nil, nil
                },
            },
            /* Получение списка продуктов
               http://localhost:8080/product?query={list{id,name,info,price}}
            */
            "list": &graphql.Field{
                Type:        graphql.NewList(productType),
                Description: "Get product list",
                Resolve: func(params graphql.ResolveParams) (interface{}, error) {
                    return products, nil
                },
            },
        },
    })

Тип queryType содержить обязательные поля Name и Fields, а также необязательное Description (используется для документации)
В свою очередь поле Fields также содержит обязательное поле Type и не обязательные поля Args, Resolve и Description


Args (Аргументы)


Аргументы — список параметров, переданных с клиента на сервер и влияющие на результат возвращаемых данных. Аргументы привязаны к конкретному полю. Причем аргументы можно передать как в Query так и в Mutation.


?query={product(id:1){name,info,price}}

В данном случае аргумент id для поля product со значением 1, говорит о том, что необходимо вернуть товар с указанным идентификатором.
Для list аргументы опущены, но в реальном приложении это могут быть, к примеру: limit и offset.


Resolve (Распознаватели)


Вся логика работы с данными (например запросы к БД, обработка и фильтрация) находится в распознователях, именно они возвращают данные, которые будут переданы на клиент в качестве ответа на запрос.


Type (Система типов)


GraphQL использует свою систему типов для описания данных. Можно использовать как базовые типы String, Int, Float, Boolean, так и собственные (пользовательские). Для нашего примера понадобится пользовательский тип Product, который будет описывать все свойства продукта


var productType = graphql.NewObject(
    graphql.ObjectConfig{
        Name: "Product",
        Fields: graphql.Fields{
            "id": &graphql.Field{
                Type: graphql.Int,
            },
            "name": &graphql.Field{
                Type: graphql.String,
            },
            "info": &graphql.Field{
                Type: graphql.String,
            },
            "price": &graphql.Field{
                Type: graphql.Float,
            },
        },
    },
)

Для каждого поля указан базовый тип, в данном случае это graphql.Int, graphql.String, graphql.Float.
Кол-во вложенных полей не ограничено, благодаря чему можно реализовывать систему графов любого уровня.


Mutation (Мутации)


Мутации эти изменяемые данные, к которым относится: добавление, редактирование и удаление. Во всем остальном мутации очень похожи на обычные запросы: они также принимают аргументы Args и возвращают данные Resolve в качестве ответа на запрос.


Давайте напишем мутации для наших товаров
  var mutationType = graphql.NewObject(graphql.ObjectConfig{
    Name: "Mutation",
    Fields: graphql.Fields{
        /* Добавление нового продукта
           http://localhost:8080/product?query=mutation+_{create(name:"Tequila",info:"Alcohol",price:99){id,name,info,price}}
        */
        "create": &graphql.Field{
            Type:        productType,
            Description: "Create new product",
            Args: graphql.FieldConfigArgument{
                "name": &graphql.ArgumentConfig{
                    Type: graphql.NewNonNull(graphql.String), // поле обязательное для заполнения
                },
                "info": &graphql.ArgumentConfig{
                    Type: graphql.String, // не обязательное поле
                },
                "price": &graphql.ArgumentConfig{
                    Type: graphql.NewNonNull(graphql.Float),
                },
            },
            Resolve: func(params graphql.ResolveParams) (interface{}, error) {
                rand.Seed(time.Now().UnixNano())
                product := Product{
                    ID:    int64(rand.Intn(100000)), // генерируем случайный ID
                    Name:  params.Args["name"].(string),
                    Info:  params.Args["info"].(string),
                    Price: params.Args["price"].(float64),
                }
                products = append(products, product)
                return product, nil
            },
        },

        /* Редактирование продукта по id
           http://localhost:8080/product?query=mutation+_{update(id:1,price:195){id,name,info,price}}
        */
        "update": &graphql.Field{
            Type:        productType,
            Description: "Update product by id",
            Args: graphql.FieldConfigArgument{
                "id": &graphql.ArgumentConfig{
                    Type: graphql.NewNonNull(graphql.Int),
                },
                "name": &graphql.ArgumentConfig{
                    Type: graphql.String,
                },
                "info": &graphql.ArgumentConfig{
                    Type: graphql.String,
                },
                "price": &graphql.ArgumentConfig{
                    Type: graphql.Float,
                },
            },
            Resolve: func(params graphql.ResolveParams) (interface{}, error) {
                id, _ := params.Args["id"].(int)
                name, nameOk := params.Args["name"].(string)
                info, infoOk := params.Args["info"].(string)
                price, priceOk := params.Args["price"].(float64)
                product := Product{}
                for i, p := range products {
                    // Редактируем информацию о продукте
                    if int64(id) == p.ID {
                        if nameOk {
                            products[i].Name = name
                        }
                        if infoOk {
                            products[i].Info = info
                        }
                        if priceOk {
                            products[i].Price = price
                        }
                        product = products[i]
                        break
                    }
                }
                return product, nil
            },
        },

        /* Удаление продукта по id
           http://localhost:8080/product?query=mutation+_{delete(id:1){id,name,info,price}}
        */
        "delete": &graphql.Field{
            Type:        productType,
            Description: "Delete product by id",
            Args: graphql.FieldConfigArgument{
                "id": &graphql.ArgumentConfig{
                    Type: graphql.NewNonNull(graphql.Int),
                },
            },
            Resolve: func(params graphql.ResolveParams) (interface{}, error) {
                id, _ := params.Args["id"].(int)
                product := Product{}
                for i, p := range products {
                    if int64(id) == p.ID {
                        product = products[i]
                        // Удаляем из списка продуктов
                        products = append(products[:i], products[i+1:]...)
                    }
                }

                return product, nil
            },
        },
    },
  })

Все по аналогии с queryType. Есть только одна маленькая особенность тип graphql.NewNonNull(graphql.Int), который сообщает нам, что данное поле не может быть пустым (похоже на NOT NULL в MySQL)


Все. Теперь у нас есть простое CRUD API на Go для работы с товарами. Мы не использовали базу данных для этого примера, но мы рассмотрели как создать модель данных и манипулировать ими с помощью мутаций.


Примеры


Если вы скачали исходники через


go get github.com/graphql-go/graphql

достаточно перейти в директорию с примером


cd examples/crud

и запустить приложение


go run main.go

Вы можете использовать следующие запросы:
Получение продукта по ID
http://localhost:8080/product?query={product(id:1){name,info,price}}


Получение списка продуктов
http://localhost:8080/product?query={list{id,name,info,price}}


Добавление нового продукта
http://localhost:8080/product?query=mutation+_{create(name:"Tequila",info:"Strong alcoholic beverage",price:999){id,name,info,price}}


Редактирование продукта
http://localhost:8080/product?query=mutation+_{update(id:1,price:195){id,name,info,price}}


Удаление продукта по id
http://localhost:8080/product?query=mutation+_{delete(id:1){id,name,info,price}}


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

Комментарии (9)


  1. zelenin
    25.07.2018 17:56

    Есть еще вот такая библиотека https://github.com/graph-gophers/graphql-go
    И она, имхо, поинтереснее.
    Есть поддержка схем, описанных на родном dsl. Есть сторонний генератор всего бойлерплейта.


  1. uzweber
    25.07.2018 21:41

    Мне кажется, что обработка GraphQL запросов дорого может обойтись
    Бенчмарки есть какие-нибудь? сколько можно потерять в производительности, по сравнению с обычными http запросами?


    1. zelenin
      26.07.2018 02:37

      по сравнению с обычными http запросами

      GraphQL — это тоже обычный http-запрос.


      1. uzweber
        26.07.2018 07:19

        Извините, не правильно выразился, по сравнению с REST-запросами имелось ввиду


        1. artyomturkin
          26.07.2018 09:42

          Сделал простой бенчмарк для сравнения graphql-go с REST на net/http

          Результаты:
          goos: windows
          goarch: amd64
          pkg: ser/graphql_bench
          BenchmarkGraphQLHTTP-4 2000 891501 ns/op
          BenchmarkHTTP-4 2000 593000 ns/op


          Разница ~0.3 миллисекунды

          Только graphql-go, по сравнению с маршалингом в json и одним REST сервисом, увеличивает время обработки в 1.5 раза на очень простом тесте, а при более сложных, скорее всего, будет еще медленнее. Но это будет только несколько миллисекунд. В зависимости от задачи, это вполне допустимый минус.

          Из-за принципа использования graphql и различных паттернов, как dataloader с кэшем, можно получить даже прирост производительности, по сравнению с использованием множества REST сервисов, для получения одинаковых данных на клиенте.


          1. uzweber
            26.07.2018 11:03

            Спасибо за бенчмарк!

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


            Если уже начать использовать протокол HTTP/2, то можно посылать несколько запросов во время одного соединения. Как по мне, вот это преимущество у graphql (получение связанных данных в одном запросе) какое-то сомнительное становится при использовании HTTP/2.


            1. artyomturkin
              26.07.2018 11:42

              Согласен. С точки зрения рантайм производительности, graphql теряет здесь преимущество.
              В зависимости от API, в graphql еще можно выиграть на размере ответов, но это уже в редких случаях даст что-то существенное.

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

              Все-таки его создала компания у которой начались проблемы больше организационные в разработке и поддержке API и фронт-систем, нежели связанные с производительностью софта (хотя и здесь они были, т.к. еще не было HTTP/2).

              К тому же есть отличные инструменты, которые сильно помогают на этапах анализа и разработки, как playground и voyager


    1. blind_oracle
      26.07.2018 09:37

      По сравнению с запросом в БД время обработки GraphQL стремится к нулю.


    1. idcooldi
      26.07.2018 10:44

      Тут перевод статьи GraphQL vs. REST, возможно что-то Вам будет интересно. Бенчмарки не нашёл.