【Go】APIサーバの構築 ~GraphQL編~

#はじめに
APIサーバにGraphQLを導入してみたのでメモを残します。

https://qiita.com/suzuki0430/items/f4396a337209cd1e37b7

https://qiita.com/suzuki0430/items/3eacf2329e0ce85d23d7

https://qiita.com/suzuki0430/items/f8308db9220c7bf19fa2

コード全文は以下です。

https://github.com/suzuki0430/backend-go-movies

#GraphQL
どこにでも書いてある情報ですが、GraphQLの導入メリットには以下のようなものがあります。

- エンドポイントが1つでOK(RESTのようなエンドポイントの肥大化を防げる)
- クエリを用いて必要なデータのみを取ってくることができる(無駄な通信帯域を消費しない)
- データ型の定義がされていて、クライアント-サーバ間の食い違いを防げる

他にもGraphQL関連の記事を書いているので、もし興味があればご覧ください。

https://qiita.com/suzuki0430/items/2d6e1fa59608144b402e

https://qiita.com/suzuki0430/items/18ef1688fc89ff3ec816

#実装
GoでGraphQLを実装するにあたり、以下のライブラリを使用します。

https://github.com/graphql-go/graphql

##導入
ライブラリをインストールします。

```
go get github.com/graphql-go/graphql
```

##ルートハンドラーの追加
`routes.go`にルートハンドラーを追加します。
RESTのときとは違いエンドポイントはこの一つだけでよく、スキーマ定義の追加修正などを行う場合は`graphql.go`の中身をさわります。

```go:routes.go
router.HandlerFunc(http.MethodPost, "/v1/graphql", app.moviesGraphQL)
```

##スキーマ定義
`graphql.go`にスキーマ定義を記述します。

```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と一致する結果を返すような処理を記述します。

```go
		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
		},
```


#参考資料

https://www.udemy.com/share/104HTy3@oyP5d9lr2-g23fjayLIeeXzzF9wdeOVyStToUsV-bqDJ5GOwSzjIa08TkjVCcgwt/