Go+GraphQLでCRUD API構築(Mac) -その１-

# はじめに
客先でCRUD用APIを`GraphQL`で構築した際の知見を整理したいと思います。
不要と思われる手順は随時読み飛ばして下さい。
何かの参考になれば幸いです。

# 前提条件

- 開発OS：MacOS High Sierra
- 開発言語：Go(v1.12.3)
- API：GraphQL
- APIフレームワーク：gqlgen(v0.11.1)
- DB：MySQL
- DBフレームワーク：gorm

# 開発環境の整備
プロジェクト作成、Go拡張ライブラリのインストール、DB環境等の整備を行います。

## プロジェクトの作成
本説明では`$GOPATH/src`の直下にプロジェクトディレクトリを作成します。
プロジェクト名は`crud_api`とします。

```
cd $GOPATH/src
mkdir crud_api
```
※$GOPATH環境変数はGoのインストールディレクトリを指します。
各自お使いの環境に合わせて設定してください。

## Go拡張ライブラリのインストール
- gqlgen
- gorm
- MySQLドライバー

を順次インストールします。

### gqlgenのインストール
フレームワークはGraphQLを実装した[gqlgen](https://github.com/99designs/gqlgen)を使用します。
当初は、`v0.10.2`を使用してましたが、執筆途中で`v0.11.1`にバージョンアップされました。
触ってみた感じ、旧バージョンで「使いにくいな〜」と感じていたところが大分解消されたようです。
なので、新バージョン`v0.11.1`を前提に説明したいと思います。
また、ファイル・ディレクトリ階層が一部旧バージョンと変わったため、ご注意ください。

```
export GO111MODULE=on
go get -u github.com/99designs/gqlgen github.com/vektah/gorunpkg
```

### gormのインストール
DB操作は[gorm](https://github.com/jinzhu/gorm)を使用します。
他にも[sqlx](https://github.com/jmoiron/sqlx)等のライブラリがありますが、`sqlx`はSQLを全てハードコーディングする事になります。
`gorm`はORMとして使用することも、SQLハードコーディングもどちらも可能です。
SOA的にはAPIで発行するクエリは極力シンプルであることが望ましいため、ORMを実装可能な`gorm`を使用します。

```
go get -u github.com/jinzhu/gorm
```

### MySQLドライバーのインストール
本説明ではMySQLをDBサーバとして使用するため、
Go用のMySQLドライバーをインストールします。

```
go get -u github.com/go-sql-driver/mysql
```

## DB作成
DBはMySQLを使用します。

Oracleデモ用DBをMySQL用に組み替えたDDLを
[MySQL用のSCOTTデータベース](https://sh2.hatenablog.jp/entry/20090512)
より拝借しました。
今回必要なのは`emp`・`dept`テーブルのみです。

本説明ではDockerの使い捨てMySQLを使用しましたが、ユーザID、パスワードは適宜お使いの環境に合わせてください。

```
# DB作成
create database learning_db;
# DB確認(learning_dbが作られていることを確認)
show databases;
# 一回MySQLを抜けてlearning_dbで入り直す（パスワードは"mysql"）
exit
mysql -u root -p learning_db
# learning_dbにログインしていることを確認する
select database();
create table dept
       (deptno decimal(2) primary key,
        dname varchar(14) ,
        loc varchar(13));
create table emp
       (empno decimal(4) primary key,
        ename varchar(10),
        job varchar(9),
        mgr decimal(4),
        hiredate date,
        sal decimal(7,2),
        comm decimal(7,2),
        deptno decimal(2),
        foreign key (deptno) references dept (deptno));
begin;
insert into dept values (10,'accounting','new york');
insert into dept values (20,'research','dallas');
insert into dept values (30,'sales','chicago');
insert into dept values (40,'operations','boston');
insert into emp values (7369,'smith','clerk',7902,str_to_date('17-12-1980','%d-%m-%Y'),800,null,20);
insert into emp values (7499,'allen','salesman',7698,str_to_date('20-2-1981','%d-%m-%Y'),1600,300,30);
insert into emp values (7521,'ward','salesman',7698,str_to_date('22-2-1981','%d-%m-%Y'),1250,500,30);
insert into emp values (7566,'jones','manager',7839,str_to_date('2-4-1981','%d-%m-%Y'),2975,null,20);
insert into emp values (7654,'martin','salesman',7698,str_to_date('28-9-1981','%d-%m-%Y'),1250,1400,30);
insert into emp values (7698,'blake','manager',7839,str_to_date('1-5-1981','%d-%m-%Y'),2850,null,30);
insert into emp values (7782,'clark','manager',7839,str_to_date('9-6-1981','%d-%m-%Y'),2450,null,10);
insert into emp values (7788,'scott','analyst',7566,date_add(str_to_date('13-jul-87','%d-%b-%y'), interval -85 day),3000,null,20);
insert into emp values (7839,'king','president',null,str_to_date('17-11-1981','%d-%m-%Y'),5000,null,10);
insert into emp values (7844,'turner','salesman',7698,str_to_date('8-9-1981','%d-%m-%Y'),1500,0,30);
insert into emp values (7876,'adams','clerk',7788,date_add(str_to_date('13-jul-87', '%d-%b-%y'), interval -51 day),1100,null,20);
insert into emp values (7900,'james','clerk',7698,str_to_date('3-12-1981','%d-%m-%Y'),950,null,30);
insert into emp values (7902,'ford','analyst',7566,str_to_date('3-12-1981','%d-%m-%Y'),3000,null,20);
insert into emp values (7934,'miller','clerk',7782,str_to_date('23-1-1982','%d-%m-%Y'),1300,null,10);
commit;
```

# API開発
やっと本題に入りました。
[公式チュートリアル](https://gqlgen.com/getting-started/)に従って進めます。

- Modules初期化
- スキーマの定義
- gqlgen初期化
- コード自動生成
- 業務ロジックの実装
- 動作検証

の順に説明します。

## Modules初期化
`go mod init`コマンドを実行し、Modulesの初期化をします。
実行後に`go.mod`ファイルが作成されます。

```
cd $GOPATH/src/crud_api/
go mod init crud_api
cat go.mod
```

## スキーマの定義
公式チュートリアルでは`sqlgenの初期化`を先に行ってますが、先に初期化を行った場合、ビルトインのサンプルコードが作成され、それが後々まで残ってしまいます。
サンプルソースに足を引っ張られるのはメンテナンス性が悪いため、あえて`スキーマの定義`を先に行います。
自分はスキーマを「機能要件を満たすために必要な何らかの意味のあるオブジェクトの集合体」だと捉えています。
本説明では検索条件・条件、検索・登録・更新結果、ファンクション定義等をスキーマとして扱います。
gqlgenではスキーマを上記で作成された`schema.graphqls`で管理します。
`graph`ディレクトリを作成し、`schema.graphqls`を作成・編集します。

```
cd $GOPATH/src/crud_api/
mkdir graph
vi graph/schema.graphqls
```

`emp`表のスキーマ`type`ブロックで定義します。

```
type Emp { 
  empno:     ID!
  ename:     String!
  job:       String!
  mgr:       Int!
  hiredate:  DateTime!
  sal:       Float!
  comm:      Float!
  deptno:   [Dept!]!
}
```
テーブル名の先頭は大文字
カラムの方は
プライマリキー → `ID`型
文字列型 → `String`型
金額系 → `Float`型
日付系 → `DateTime`型
で定義します。

次に検索条件を`input`ブロックで定義します。
今回は`従業員番号(empno)`・`従業員名(ename)`・`所属部署(deptno)`をパラメータで受け取り、それを条件に`emp`表を検索する処理を作成します。

まずは検索条件を定義します。

```
# 検索・更新条件
input CondEmp {
  empno:     Int
  ename:     String
  deptno:    Int
}
```

検索処理は`Query`ブロックで定義します。
クエリ名は`selectEmp `にします。
上記で定義した`CondEmp`を受け取り、`Emp型`のスライスを返却するように定義します。

```
type Query {
  selectEmp(
    input: CondEmp
  ): [Emp!]!
}
```

gqlgenで実装されたスカラー型の`DateTime`を使用するため、
scalar宣言を追加します。

```
scalar DateTime
```

で、ひとまず`schema.graphqls`の編集は終了です。

## gqlgen初期化
続けて下記の`gqlgen`の初期化コマンドを実行します。

```
go run github.com/99designs/gqlgen init
```

すると`crud_api`配下に下記のようなファイル・ディレクトリ構成が出来上がると思います。

```
├── go.mod
├── go.sum
├── gqlgen.yml
├── graph
│   ├── generated
│   │   └── generated.go
│   ├── model
│   │   └── models_gen.go ★モデルが生成される ※手動で編集はNG
│   ├── resolver.go ★Resolverとして保持する変数等をを定義
│   ├── schema.graphqls ★スキーマを記述する
│   └── schema.resolvers.go ★業務処理を記述する
└── server.go ★WEBサーバ
```

主に使用するファイルは「★」マークをつけたファイルです。

`schema.resolvers.go`をエディタで開くと以下のようになってます。

```
package graph

import (
	"context"
	"crud_api/graph/generated"
	"crud_api/graph/model"
	"fmt"
)

func (r *queryResolver) SelectEmp(ctx context.Context, input *model.CondEmp) ([]*model.Emp, error) {
	panic(fmt.Errorf("not implemented"))
}

func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }

type queryResolver struct{ *Resolver }
```

`schema.graphqls`で定義した`SelectEmp`がファンクションとして作成されていることがわかります。

次に`graph/model/models_gen.go`の内容を確認します。

```
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.

package model

type CondEmp struct {
	Empno  *int    `json:"empno"`
	Ename  *string `json:"ename"`
	Deptno *int    `json:"deptno"`
}

type Emp struct {
	Empno    string  `json:"empno"`
	Ename    string  `json:"ename"`
	Job      string  `json:"job"`
	Mgr      int     `json:"mgr"`
	Hiredate string  `json:"hiredate"`
	Sal      float64 `json:"sal"`
	Comm     float64 `json:"comm"`
	Deptno   int     `json:"deptno"`
}
```

`schema.graphqls`で定義した`CondEmp`と`Emp`が構造体として作成されていることがわかります。


## 業務ロジックの実装
旧バージョンの場合、ビジネスロジックを`resolver.go`に実装しましたが、`v0.11.1`からは`schema.resolvers.go`に実装します。

先ほど確認した`schema.resolvers.go`を開き、`SelectEmp`ファンクションの`panic`の一行を削除し、ここに実際の検索処理を実装していきます。

まずはMySQL接続処理を記載します。

ここでは説明の便宜上、DBホスト・ユーザーID・パスワードをハードコーディングしていますが、実際の業務ではまずやりません。
環境変数・設定ファイル等に外出しするのが本来望ましいですが、その説明は次回にさせて頂きます。

また、DBホスト・ユーザーID・パスワードは各自環境に合わせて変更して下さい。

```
func (r *queryResolver) SelectEmp(ctx context.Context, input *model.CondEmp) ([]*model.Emp, error) {
    /****************************************
     * DBコネクション取得
     ****************************************/
    //TODO お使いの環境に合わせて記述を変更してください（ユーザ：パスワード@tcp(ホスト名:ポート)/DB名）
    connect := "root:mysql@tcp(localhost:3306)/learning_db"
    db, err := gorm.Open("mysql", connect)
    if err != nil {
        fmt.Println(err)
        panic("DB Connect Error.")
    }
    defer db.Close()
    // SQLダンプ有効
    db.LogMode(true)
    // テーブル名は"emp"のままとする（デフォルト設定の場合"emps"に暗黙変換される）
    db.SingularTable(true)
```

次に検索条件を実装します。

```
    /****************************************
     * 検索条件の作成
     ****************************************/
    where := model.Emp{}
    if input.Empno != nil {
        where.Empno = strconv.Itoa(*input.Empno)
    }
    if input.Ename != nil {
        where.Ename = *input.Ename
    }
    if input.Deptno != nil {
        where.Deptno = *input.Deptno
    }
```
パラメータで`従業員番号(empno)`・`従業員名(ename)`・`所属部署(deptno)`のいずれかを受け取り、`where`マップに詰め込んで検索条件を作成します。

次に検索処理を定義して`SelectEmp`ファンクションの実装は終わります。

```
    /****************************************
     * 検索処理の実行
     ****************************************/
    db.Where(where).Find(&r.emps)
    return r.emps, nil
}
```

## 動作検証
gqlgenビルトインのWebサーバーを立ち上げて動作確認をします。
`crud_api`直下に作成された`server.go`をRUNさせることで起動させる事が出来ます。

```
cd $GOPATH/src/crud_api/
go run server.go
```

ブラウザでhttp://localhost:8080/ にアクセスすると下記の画面が立ち上がります。

※起動エラーになった場合はコンパイルエラーになっている可能性があります。
エラーメッセージを参考にしながらソースを修正してください。

<img width="824" alt="aaa.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573734/dae24729-c978-5358-c6a5-87e8c7bd7e8f.png">

左側のペインにGraphQLのクエリを入力し、真ん中の矢印ボタンを押下すると、対象のAPIが起動され、実行結果が右側のペインに表示されます。

`emp`テーブルから指定の`empno`に紐づくレコードを取得するクエリを左ペインに入力し、実行します。

```
query{
  selectEmp(
    input:{
      empno:7369
  	}
  )
  {
  	empno
  	ename
  	job
  	mgr
  	hiredate
  	sal
  	comm
    deptno
  }
}
```

`emp`テーブルの検索結果が右ペインに出力されていることがわかります。
<img width="1002" alt="bbb.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573734/ea3bf7d7-1489-de0a-118d-c6bc0e209e46.png">


# さいごに
前準備の説明に多くをさき、本編はSELECT処理のみしか説明出来なかったので、残りのINSERT/UPDATE/DELETE処理の説明は次回に繰り越したいと思います。
また、DBコネクション取得処理もセキュアな情報がソース内にベタ書きであること、扱うテーブルやCRUD処理が増えた際に`schema.resolvers.go`の実装量・冗長性が増していく等、実務で使える実装になっていません。
実務を想定した実装も次回に説明させて頂きたいと思います。

以上ですが、最後までお読みいただきありがとうございました。

# 参考サイト
[公式チュートリアル](https://gqlgen.com/getting-started/)
[gqlgenのスタートアップサイト](https://gqlgen.com/getting-started/)
[GoのGraphQLライブラリのgqlgenのGetting startedをやってみた](http://chidakiyo.hatenablog.com/entry/graphql-gqlgen-getting-started)
[gqlgenのチュートリアルを試しました](https://qiita.com/nisitanisubaru/items/a61af89f2a531bb17687)
[GraphQLでタスク管理アプリを作る -バックエンド編- [Go + gqlgen]]
(https://qiita.com/ebkn/items/0b30bdbf0dae5df73d2e)
[MySQL用のSCOTTデータベース](https://sh2.hatenablog.jp/entry/20090512)
[GraphQLでカスタムスカラー型を作る](http://webtech-walker.com/archive/2016/02/graphql-custom-scalar-type.html)