GraphQL API (CRUD) на Go

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 как на возможную альтернативу. Да, с первого взгляда это кажется сложнее, но стоит начать и за пару дней вы освоите данную технологию. Как минимум это будет полезно.

© Habrahabr.ru