Skip to content

GraphQL に入門した(Go)

2021-04-14


はじめに

最近、GraphQL に入門したので Go でサーバサイドを実装しました。

GraphQL に入門した(概念)で触れた内容は省略します。

最終的なコードは以下のようになります。

ディレクトリ構成としては以下のようになっています。

  • db
    • データベース接続用の関数が入っている
  • model
    • struct が入っている
  • repository
    • データベースアクセスをするための関数が入っている
  • schema
    • GraphQL のスキーマが定義されている
    • Query も Mutation も同じ
  • main.go
    • 実行

実装するもの

簡単な TODO リストを実装してみたいと思います。
データの形式は以下のような形になっています。

MariaDB > show columns from todo;
+------------+---------------+------+-----+---------------------+----------------+
| Field      | Type          | Null | Key | Default             | Extra          |
+------------+---------------+------+-----+---------------------+----------------+
| id         | int(11)       | NO   | PRI | NULL                | auto_increment |
| title      | varchar(127)  | YES  |     | NULL                |                |
| content    | varchar(1023) | YES  |     | NULL                |                |
| is_active  | tinyint(1)    | NO   |     | 1                   |                |
| created_at | timestamp     | NO   |     | current_timestamp() |                |
| updated_at | timestamp     | NO   |     | current_timestamp() |                |
+------------+---------------+------+-----+---------------------+----------------+
6 rows in set (0.001 sec)
1
2
3
4
5
6
7
8
9
10
11
12

データベースとの接続

まずはローカルに DB を作っているのでそれと接続する関数を定義します。 この関数をデータベースとやりとりするたびに呼び出すという感じです。

// db/init.go

package db

import (
	"os"

    _ "github.com/go-sql-driver/mysql"
    "github.com/jinzhu/gorm"
)

func DBConn() (*gorm.DB, error) {
	DBMS := "mysql" // mariadb
	HOSTNAME := os.Getenv("HOSTNAME")
	USERNAME := os.Getenv("USERNAME")
	DBNAME := os.Getenv("DB_NAME")
	PASSWORD := os.Getenv("PASSWORD")
	PORT := os.Getenv("PORT")

	CONNECT := USERNAME + ":" + PASSWORD + "@(" + HOSTNAME + ":" + PORT + ")/" + DBNAME + "?parseTime=true"
	db, err := gorm.Open(DBMS, CONNECT)
	if err != nil {
		return nil, err
	}

	return db, nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

Query を定義する

次に Query を定義します。
Query を定義するためにまずは型を定義します。
当然ですが型はデータベースの型と合わせましょう。

// schema/schema.go

var TodoType = graphql.NewObject(graphql.ObjectConfig{
	Name: "Todo",
	Fields: graphql.Fields{
		"id": &graphql.Field{
			Type: graphql.Int,
		},
		"title": &graphql.Field{
			Type: graphql.String,
		},
		"content": &graphql.Field{
			Type: graphql.String,
		},
		"is_active": &graphql.Field{
			Type: graphql.Boolean,
		},
		"created_at": &graphql.Field{
			Type: graphql.DateTime,
		},
		"updated_at": &graphql.Field{
			Type: graphql.DateTime,
		},
	},
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

次にフィールドを定義します。フィールドは上の型を使って TODO を全件取得するものと id に紐づく TODO を取得するものの 2 パターン作成します。
TodoFields では特定の id に紐づく TODO を 1 つ使用します。
Args を使用すると引数を受け取ることができます。
Resolve はその後の処理を定義することができます。また、Resolve は GraphQL のレスポンスにもなります。

TodosFieldsTodoFields よりもいくらかシンプルです。
Resolve の中で全件取得するようの関数を呼び出してそのまま戻り値としています。

// schema/schema.go

var TodoFields = &graphql.Field{
	Type:        TodoType,
	Description: "get post detail",
	Args: graphql.FieldConfigArgument{
		"id": &graphql.ArgumentConfig{
			Type: graphql.Int,
		},
	},
	Resolve: func(p graphql.ResolveParams) (interface{}, error) {
		id, ok := p.Args["id"].(int)
		if ok {
			post, err := repository.GetTodo(id)
			if err != nil {
				return model.Todo{}, nil
			}
			return post, nil
		}
		return model.Todo{}, nil
	},
}

var TodosFields = &graphql.Field{
	Type:        graphql.NewList(TodoType),
	Description: "get all post",
	Resolve: func(p graphql.ResolveParams) (interface{}, error) {
		return repository.GetTodos(), nil
	},
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

Mutation を定義する

Mutation も同様に定義することができます。
型は先ほどと同様に定義することができるため、フィールドのみの実装となります。
とは言ってもやってることは変わらないのでそこまで難しくないのかなと思います。
TODO を作成するための CreateTodoFields と更新するための UpdateTodoFields の 2 つを定義します。

CreateTodoFieldsUpdateTodoFields 共に先ほどと同様 Args で引数を取得します。

// schema/schema.go

var CreateTodoFields = &graphql.Field{
	Type:        TodoType,
	Description: "Create new todo",
	Args: graphql.FieldConfigArgument{
		"title": &graphql.ArgumentConfig{
			Type: graphql.NewNonNull(graphql.String),
		},
		"content": &graphql.ArgumentConfig{
			Type: graphql.NewNonNull(graphql.String),
		},
		"is_active": &graphql.ArgumentConfig{
			Type: graphql.NewNonNull(graphql.Boolean),
		},
	},
	Resolve: func(params graphql.ResolveParams) (interface{}, error) {
		title, _ := params.Args["title"].(string)
		content, _ := params.Args["content"].(string)
		isActive, _ := params.Args["is_active"].(bool)

		_newTodo := model.Todo{
			Title:    title,
			Content:  content,
			IsActive: isActive,
		}

		newTodo, err := repository.CreateTodo(_newTodo)
		if err != nil {
			fmt.Println("create data faild")
		}

		return newTodo, nil
	},
}

var UpdateTodoFields = &graphql.Field{
	Type:        TodoType,
	Description: "Create new todo",
	Args: graphql.FieldConfigArgument{
		"id": &graphql.ArgumentConfig{
			Type: graphql.NewNonNull(graphql.Int),
		},
		"title": &graphql.ArgumentConfig{
			Type: graphql.NewNonNull(graphql.String),
		},
		"content": &graphql.ArgumentConfig{
			Type: graphql.NewNonNull(graphql.String),
		},
		"is_active": &graphql.ArgumentConfig{
			Type: graphql.NewNonNull(graphql.Boolean),
		},
	},
	Resolve: func(params graphql.ResolveParams) (interface{}, error) {
		id := int64(params.Args["id"].(int)) // ちょっと汚い
		title, _ := params.Args["title"].(string)
		content, _ := params.Args["content"].(string)
		isActive, _ := params.Args["is_active"].(bool)

		_updateTodo := model.Todo{
			Id:       id,
			Title:    title,
			Content:  content,
			IsActive: isActive,
		}

		updateTodo, err := repository.UpdateTodo(_updateTodo)
		if err != nil {
			fmt.Println("update data faild")
		}

		return updateTodo, nil
	},
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

Schema の定義

最後にこれらを 1 つの Schema として定義します。
先ほど一生懸命定義した関数を当てはめるだけです、簡単です。

// schema/schema.go

var Schema = graphql.SchemaConfig{
	Query: graphql.NewObject(
		graphql.ObjectConfig{
			Name: "TodoQuery",
			Fields: graphql.Fields{
				"getTodo":  TodoFields,
				"getTodos": TodosFields,
			},
		},
	),
	Mutation: graphql.NewObject(
		graphql.ObjectConfig{
			Name: "TodoMutation",
			Fields: graphql.Fields{
				"createTodo": CreateTodoFields,
				"updateTodo": UpdateTodoFields,
			},
		},
	),
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

最後に schema.go で実装した内容をまとめると以下のようになります。

// schema/schema.go

package schema

import (
	"fmt"
	"graphql_suburi/backend/model"
	"graphql_suburi/backend/repository"

	"github.com/graphql-go/graphql"
)

var TodoType = graphql.NewObject(graphql.ObjectConfig{
	Name: "Post",
	Fields: graphql.Fields{
		"id": &graphql.Field{
			Type: graphql.Int,
		},
		"title": &graphql.Field{
			Type: graphql.String,
		},
		"content": &graphql.Field{
			Type: graphql.String,
		},
		"is_active": &graphql.Field{
			Type: graphql.Boolean,
		},
		"created_at": &graphql.Field{
			Type: graphql.DateTime,
		},
		"updated_at": &graphql.Field{
			Type: graphql.DateTime,
		},
	},
})

var TodoFields = &graphql.Field{
	Type:        TodoType,
	Description: "get post detail",
	Args: graphql.FieldConfigArgument{
		"id": &graphql.ArgumentConfig{
			Type: graphql.Int,
		},
	},
	Resolve: func(p graphql.ResolveParams) (interface{}, error) {
		id, ok := p.Args["id"].(int)
		if ok {
			post, err := repository.GetTodo(id)
			if err != nil {
				return model.Todo{}, nil
			}
			return post, nil
		}
		return model.Todo{}, nil
	},
}

var TodosFields = &graphql.Field{
	Type:        graphql.NewList(TodoType),
	Description: "get all post",
	Resolve: func(p graphql.ResolveParams) (interface{}, error) {
		return repository.GetTodos(), nil
	},
}

var CreateTodoFields = &graphql.Field{
	Type:        TodoType,
	Description: "Create new todo",
	Args: graphql.FieldConfigArgument{
		"title": &graphql.ArgumentConfig{
			Type: graphql.NewNonNull(graphql.String),
		},
		"content": &graphql.ArgumentConfig{
			Type: graphql.NewNonNull(graphql.String),
		},
		"is_active": &graphql.ArgumentConfig{
			Type: graphql.NewNonNull(graphql.Boolean),
		},
	},
	Resolve: func(params graphql.ResolveParams) (interface{}, error) {

		title, _ := params.Args["title"].(string)
		content, _ := params.Args["content"].(string)
		isActive, _ := params.Args["is_active"].(bool)

		_newTodo := model.Todo{
			Title:    title,
			Content:  content,
			IsActive: isActive,
		}

		newTodo, err := repository.CreateTodo(_newTodo)
		if err != nil {
			fmt.Println("create data faild")
		}

		return newTodo, nil
	},
}

var UpdateTodoFields = &graphql.Field{
	Type:        TodoType,
	Description: "Create new todo",
	Args: graphql.FieldConfigArgument{
		"id": &graphql.ArgumentConfig{
			Type: graphql.NewNonNull(graphql.Int),
		},
		"title": &graphql.ArgumentConfig{
			Type: graphql.NewNonNull(graphql.String),
		},
		"content": &graphql.ArgumentConfig{
			Type: graphql.NewNonNull(graphql.String),
		},
		"is_active": &graphql.ArgumentConfig{
			Type: graphql.NewNonNull(graphql.Boolean),
		},
	},
	Resolve: func(params graphql.ResolveParams) (interface{}, error) {
		id := int64(params.Args["id"].(int)) // ちょっと汚い
		title, _ := params.Args["title"].(string)
		content, _ := params.Args["content"].(string)
		isActive, _ := params.Args["is_active"].(bool)

		_updateTodo := model.Todo{
			Id:       id,
			Title:    title,
			Content:  content,
			IsActive: isActive,
		}

		updateTodo, err := repository.UpdateTodo(_updateTodo)
		if err != nil {
			fmt.Println("update data faild")
		}

		return updateTodo, nil
	},
}

var Schema = graphql.SchemaConfig{
	Query: graphql.NewObject(
		graphql.ObjectConfig{
			Name: "TodoQuery",
			Fields: graphql.Fields{
				"getTodo":  TodoFields,
				"getTodos": TodosFields,
			},
		},
	),
	Mutation: graphql.NewObject(
		graphql.ObjectConfig{
			Name: "TodoMutation",
			Fields: graphql.Fields{
				"createTodo": CreateTodoFields,
				"updateTodo": UpdateTodoFields,
			},
		},
	),
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159

呼び出す

ここまで定義したきたのであとは呼び出すだけです。
main.go で net/http を使用してサーバを立ち上げ動作確認をしましょう。
25 行目ではバリデーションをかけています。ここでリクエストの形式が違ったり不正があったりしたら弾き、CORS エラーになるようにしています。
また、35 行目からの graphql.Do() で GraphQL を実行しています。

// main.go

package main

import (
	"encoding/json"
	"fmt"
	"graphql_suburi/backend/model"
	"graphql_suburi/backend/schema"
	"log"
	"net/http"

	_ "github.com/go-sql-driver/mysql"
	"github.com/graphql-go/graphql"
)

func main() {
	http.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Headers", "*")
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")

		var p model.PostData
		if r.Method == "OPTIONS" {
		} else if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
			w.WriteHeader(400)
			return
		}

		schema, err := graphql.NewSchema(schema.Schema)
		if err != nil {
			log.Fatalf("failed to get schema, error: %v", err)
		}

		result := graphql.Do(graphql.Params{
			Context:        r.Context(),
			Schema:         schema,
			RequestString:  p.Query,
			VariableValues: p.Variables,
			OperationName:  p.Operation,
		})

		if err := json.NewEncoder(w).Encode(result); err != nil {
			fmt.Printf("could not write result to response: %s", err)
		}
	})

	fmt.Println("listening on :8888 ...")
	if err := http.ListenAndServe(":8888", nil); err != nil {
		log.Fatalln(err)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

動作確認

動作確認をします。curl でリクエストを投げたいと思います。

TODO を全件取得

リクエスト

curl -X POST -H "Content-Type: application/json" --data '{ "query": "{ getTodos { id title content  } }" }' http://localhost:8888/graphql
1

レスポンス

{
  "data": {
    "getTodos": [
      { "content": "hoge", "id": 1, "title": "takumi" },
      { "content": "marinyan", "id": 2, "title": "marina" },
      { "content": "takurinton", "id": 3, "title": "test3" }
    ]
  }
}
1
2
3
4
5
6
7
8
9

id が 1 の TODO を取得

リクエスト

curl -X POST -H "Content-Type: application/json" --data '{ "query": "{ getTodo(id:1) { id title content  } }" }' http://localhost:8888/graphql
1

レスポンス

{ "data": { "getTodo": { "content": "hoge", "id": 1, "title": "takumi" } } }
1

TODO を作成する

リクエスト

curl -X POST -H "Content-Type: application/json" --data '{ "query": "mutation { createTodo(title:\"takurinton\",content:\"wakuwakuwakuwaku\",is_active:true) { id title content is_active created_at } }" }' http://localhost:8888/graphql
1

レスポンス

{
  "data": {
    "createTodo": {
      "content": "wakuwakuwakuwaku",
      "created_at": "2021-04-15T12:57:22.5392956Z",
      "id": 4,
      "is_active": true,
      "title": "takurinton"
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11

id が 1 の TODO を更新

リクエスト

curl -X POST -H "Content-Type: application/json" --data '{ "query": "mutation { updateTodo(id:1,title:\"takumi katayama\",content:\"hoge\",is_active:false) { id title content is_active created_at } }" }' http://localhost:8888/graphql
1

レスポンス

{
  "data": {
    "updateTodo": {
      "content": "hoge",
      "created_at": "0001-01-01T00:00:00Z",
      "id": 1,
      "is_active": false,
      "title": "takumi katayama"
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11

このような形でそれぞれのメソッドがしっかり動いてることを確認することができました。めでたしめでたし。

まとめ

リクエストやレスポンスに型を持たせることができるのは非常に体験が良くなるなと感じました。
ただ、自分の中でまだまだ良さに気づけていない部分や深めなければいけない部分、ベストプラクティス、一般的な書き方についての理解が足りていないのでもっと頑張ってやっていきたいと思います。