Всем привет! О 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)
uzweber
25.07.2018 21:41Мне кажется, что обработка GraphQL запросов дорого может обойтись
Бенчмарки есть какие-нибудь? сколько можно потерять в производительности, по сравнению с обычными http запросами?zelenin
26.07.2018 02:37по сравнению с обычными http запросами
GraphQL — это тоже обычный http-запрос.
uzweber
26.07.2018 07:19Извините, не правильно выразился, по сравнению с REST-запросами имелось ввиду
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 сервисов, для получения одинаковых данных на клиенте.uzweber
26.07.2018 11:03Спасибо за бенчмарк!
по сравнению с использованием множества REST сервисов, для получения одинаковых данных на клиенте.
Если уже начать использовать протокол HTTP/2, то можно посылать несколько запросов во время одного соединения. Как по мне, вот это преимущество у graphql (получение связанных данных в одном запросе) какое-то сомнительное становится при использовании HTTP/2.artyomturkin
26.07.2018 11:42Согласен. С точки зрения рантайм производительности, graphql теряет здесь преимущество.
В зависимости от API, в graphql еще можно выиграть на размере ответов, но это уже в редких случаях даст что-то существенное.
По моему, GraphQL стоит больше рассматривать за его другие преимущества: гибкость запросов и статическая схема — этими преимуществами он ближе к SQL, чем к REST, и как он вписывается в общий процесс развития компании, особенно, если она большая.
Все-таки его создала компания у которой начались проблемы больше организационные в разработке и поддержке API и фронт-систем, нежели связанные с производительностью софта (хотя и здесь они были, т.к. еще не было HTTP/2).
К тому же есть отличные инструменты, которые сильно помогают на этапах анализа и разработки, как playground и voyager
idcooldi
26.07.2018 10:44Тут перевод статьи GraphQL vs. REST, возможно что-то Вам будет интересно. Бенчмарки не нашёл.
zelenin
Есть еще вот такая библиотека https://github.com/graph-gophers/graphql-go
И она, имхо, поинтереснее.
Есть поддержка схем, описанных на родном dsl. Есть сторонний генератор всего бойлерплейта.