LoginSignup
7
4

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-09

はじめに

客先で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データベース
より拝借しました。
今回必要なのはempdeptテーブルのみです。

本説明では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で定義したCondEmpEmpが構造体として作成されていることがわかります。

業務ロジックの実装

旧バージョンの場合、ビジネスロジックを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/ にアクセスすると下記の画面が立ち上がります。

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

aaa.png

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

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

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

empテーブルの検索結果が右ペインに出力されていることがわかります。
bbb.png

さいごに

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

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

参考サイト

公式チュートリアル
gqlgenのスタートアップサイト
GoのGraphQLライブラリのgqlgenのGetting startedをやってみた
gqlgenのチュートリアルを試しました
GraphQLでタスク管理アプリを作る -バックエンド編- [Go + gqlgen]
MySQL用のSCOTTデータベース
GraphQLでカスタムスカラー型を作る

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4