はじめに
客先で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を使用します。
当初は、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を使用します。
他にも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データベース
より拝借しました。
今回必要なのは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開発
やっと本題に入りました。
公式チュートリアルに従って進めます。
- 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/ にアクセスすると下記の画面が立ち上がります。
※起動エラーになった場合はコンパイルエラーになっている可能性があります。
エラーメッセージを参考にしながらソースを修正してください。
左側のペインにGraphQLのクエリを入力し、真ん中の矢印ボタンを押下すると、対象のAPIが起動され、実行結果が右側のペインに表示されます。
emp
テーブルから指定のempno
に紐づくレコードを取得するクエリを左ペインに入力し、実行します。
query{
selectEmp(
input:{
empno:7369
}
)
{
empno
ename
job
mgr
hiredate
sal
comm
deptno
}
}
emp
テーブルの検索結果が右ペインに出力されていることがわかります。
さいごに
前準備の説明に多くをさき、本編はSELECT処理のみしか説明出来なかったので、残りのINSERT/UPDATE/DELETE処理の説明は次回に繰り越したいと思います。
また、DBコネクション取得処理もセキュアな情報がソース内にベタ書きであること、扱うテーブルやCRUD処理が増えた際にschema.resolvers.go
の実装量・冗長性が増していく等、実務で使える実装になっていません。
実務を想定した実装も次回に説明させて頂きたいと思います。
以上ですが、最後までお読みいただきありがとうございました。
参考サイト
公式チュートリアル
gqlgenのスタートアップサイト
GoのGraphQLライブラリのgqlgenのGetting startedをやってみた
gqlgenのチュートリアルを試しました
[GraphQLでタスク管理アプリを作る -バックエンド編- [Go + gqlgen]]
(https://qiita.com/ebkn/items/0b30bdbf0dae5df73d2e)
MySQL用のSCOTTデータベース
GraphQLでカスタムスカラー型を作る