#はじめに
APIサーバにGraphQLを導入してみたのでメモを残します。
コード全文は以下です。
#GraphQL
どこにでも書いてある情報ですが、GraphQLの導入メリットには以下のようなものがあります。
- エンドポイントが1つでOK(RESTのようなエンドポイントの肥大化を防げる)
- クエリを用いて必要なデータのみを取ってくることができる(無駄な通信帯域を消費しない)
- データ型の定義がされていて、クライアント-サーバ間の食い違いを防げる
他にもGraphQL関連の記事を書いているので、もし興味があればご覧ください。
#実装
GoでGraphQLを実装するにあたり、以下のライブラリを使用します。
##導入
ライブラリをインストールします。
go get github.com/graphql-go/graphql
##ルートハンドラーの追加
routes.go
にルートハンドラーを追加します。
RESTのときとは違いエンドポイントはこの一つだけでよく、スキーマ定義の追加修正などを行う場合はgraphql.go
の中身をさわります。
routes.go
router.HandlerFunc(http.MethodPost, "/v1/graphql", app.moviesGraphQL)
##スキーマ定義
graphql.go
にスキーマ定義を記述します。
graphql.go
package main
import (
"backend/models"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"strings"
"github.com/graphql-go/graphql"
)
var movies []*models.Movie
// スキーマ定義
var fields = graphql.Fields{
"movie": &graphql.Field{
Type: movieType,
Description: "Get movie by id",
Args: graphql.FieldConfigArgument{
"id": &graphql.ArgumentConfig{
Type: graphql.Int,
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
id, ok := p.Args["id"].(int)
if ok {
for _, movie := range movies {
if movie.ID == id {
return movie, nil
}
}
}
return nil, nil
},
},
"list": &graphql.Field{
Type: graphql.NewList(movieType),
Description: "Get all movies",
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
return movies, nil
},
},
"search": &graphql.Field{
Type: graphql.NewList(movieType),
Description: "Search movies by title",
Args: graphql.FieldConfigArgument{
"titleContains": &graphql.ArgumentConfig{
Type: graphql.String,
},
},
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
var theList []*models.Movie
search, ok := params.Args["titleContains"].(string)
if ok {
for _, currentMovie := range movies {
if strings.Contains(currentMovie.Title, search) {
log.Println("Found one")
theList = append(theList, currentMovie)
}
}
}
return theList, nil
},
},
}
var movieType = graphql.NewObject(
graphql.ObjectConfig{
Name: "Movie",
Fields: graphql.Fields {
"id": &graphql.Field{
Type: graphql.Int,
},
"title": &graphql.Field{
Type: graphql.String,
},
"description": &graphql.Field{
Type: graphql.String,
},
"year": &graphql.Field{
Type: graphql.Int,
},
"release_date": &graphql.Field{
Type: graphql.DateTime,
},
"runtime": &graphql.Field{
Type: graphql.Int,
},
"rating": &graphql.Field{
Type: graphql.Int,
},
"mpaa_rating": &graphql.Field{
Type: graphql.String,
},
"created_at": &graphql.Field{
Type: graphql.DateTime,
},
"updated_at": &graphql.Field{
Type: graphql.DateTime,
},
},
},
)
func (app *application) moviesGraphQL(w http.ResponseWriter, r * http.Request) {
// DBから全データを取得する
movies, _ = app.models.DB.All()
// リクエストボディを読み込んでクエリをつくる
q, _ := io.ReadAll(r.Body)
query := string(q)
log.Println(query)
// スキーマをつくる
rootQuery := graphql.ObjectConfig{Name: "RootQuery", Fields: fields}
schemaConfig := graphql.SchemaConfig{Query: graphql.NewObject(rootQuery)}
schema, err := graphql.NewSchema(schemaConfig)
if err != nil {
app.errorJSON(w, errors.New("failed to create schema"))
log.Println(err)
return
}
params := graphql.Params{Schema: schema, RequestString: query}
resp := graphql.Do(params)
if len(resp.Errors) > 0 {
app.errorJSON(w, fmt.Errorf("failed: %+v", resp.Errors))
}
j, _ := json.MarshalIndent(resp, "", " ")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(j)
}
fields
にmovie
list
search
のスキーマを定義します。
Type
にはスキーマの型、Args
には引数の型、Resolve
にはクエリの結果を記述します。
例えばmovie
の場合であれば、Resolve
内にArgs
で取得したidと一致する結果を返すような処理を記述します。
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
id, ok := p.Args["id"].(int)
if ok {
for _, movie := range movies {
if movie.ID == id {
return movie, nil
}
}
}
return nil, nil
},
#参考資料