Help us understand the problem. What is going on with this article?

gormとgo-json-restを使ってDBからのデータを返すAPIサーバを作る

More than 1 year has passed since last update.

この記事は、Treasure Advent Calendar 2018の9日目の記事です!

はじめに

Go言語(golang)でDB接続からのJSONを返すAPIサーバーをサクッと作ろうと思い、見つけたライブラリたちを使って書いてみました。

【gorm】
https://github.com/jinzhu/gorm
【go-json-rest】
https://github.com/ant0ine/go-json-rest

どちらも、ドキュメントを読んだら丁寧にExampleがたくさんあるし、gormもgo-json-restもそれぞれ検索したら記事が出てくるので、それを見ていただいたら話は早いかも。

ホントにサクッと動かすだけなので、細かいところは何も考えてないです。笑

環境

macOS Mojave (10.14.1)
Golang 1.11.2
Docker 18.06.1
MySQL 8.0

Docker上のMySQLです。もう勝手に動いている体で話します。

ライブラリのインストール

$ go get github.com/jinzhu/gorm
$ go get github.com/ant0ine/go-json-rest

とりあえず全部のソースコード

今回、DBにはcomicsテーブルにマンガの情報が入っていて、
そのデータを全部引っ張り出して来るのと、
Query Parameterで引き出してくる数を指定できると言った仕様のAPIである。

main.go
package main

import (
    "github.com/ant0ine/go-json-rest/rest"
    _ "github.com/go-sql-driver/mysql"
    "github.com/jinzhu/gorm"
    "log"
    "net/http"
)

func main() {

    i := Impl{}
    i.InitDB()

    api := rest.NewApi()
    api.Use(rest.DefaultDevStack...)
    router, err := rest.MakeRouter(
        rest.Get("/api/comic/list", i.GetAllComics),
        )
    if err != nil {
        log.Fatal(err)
    }
    api.SetApp(router)
    log.Fatal(http.ListenAndServe(":8080", api.MakeHandler()))
}

type Impl struct {
    DB *gorm.DB
}

type Comic struct {
    ID                 int64  `json:"id"`
    IsbnCode           int64  `json:"isbn_code"`
    Title              string `json:"title"`
    Author             string `json:"author"`
    Publisher          string `json:"publisher"`
    Cover              string `json:"cover"`
    ReleaseDate        string `json:"release_date"`
}

func (i *Impl) InitDB() {
    var err error
    i.DB, err = gorm.Open("mysql", "{user_name}:{password}@tcp(localhost:3306)/{database_name}?parseTime=true&&loc=Asia%2FTokyo&charset=utf8")
    if err != nil {
        log.Fatalf("Got error when connect database, the error is '%v'", err)
    }
    i.DB.LogMode(true)
}

func (i *Impl) GetAllComics(w rest.ResponseWriter, r *rest.Request) {
    v := r.URL.Query()
    num := v.Get("num")

    comics := []Comic{}
    i.DB.Limit(num).Find(&comics)

    w.WriteHeader(http.StatusOK)
    w.WriteJson(&comics)
}

DBのテーブルに合わせて構造体を定義しておき、jsonタグでキー名を書いておきます。
(jsonタグを書かなければ、構造体のメンバ名が使用されます)
(構造体のメンバ名はキャメルケースで、DBのカラム名はスネークケースでも、よしなにしてくれます)

実行

上記のコードを実行すると、APIサーバの完成。
$ go run main.go

サーバが立ち上がった状態で、ブラウザ等から以下のURLを入力すると、comicテーブルの全レコードが返ってくる。
localhost:8080/api/comic/list
なお、返ってくるレコード数を、例えば10個にしたい場合は、
localhost:8080/api/comic/list?num=10
とか書いていただくと、ちゃんと動いてくれます。

少し説明

いらぬお世話かも知れないが、ちょっとだけコードの説明を挟んでおきます。
自分が初めてプログラミングをした時のことを思い出すと、理解できていないと思うので、少しだけどういうことをしているのか説明しておきます。

上記のコードにコメントを書いておきます。
間違いや補足があったら教えて下さい!!

main.go
package main

// 使っているパッケージ、ライブラリをここに書きます
import (
    "github.com/ant0ine/go-json-rest/rest"
    _ "github.com/go-sql-driver/mysql"
    "github.com/jinzhu/gorm"
    "log"
    "net/http"
)

func main() {
    // DB周り初期設定
    i := Impl{}
    // DBとの接続
    i.InitDB()

    // おまじない、、
    api := rest.NewApi()
    api.Use(rest.DefaultDevStack...)
    // ルーティング。URLから、どの処理をするのか分岐させるところ
    router, err := rest.MakeRouter(
        rest.Get("/api/comic/list", i.GetAllComics),
        // ここでは1つしかAPIがないが、例えば、
        // rest.POST("/api/comic", i.PostComicData),
        // みたいに書けば、処理を分岐させられる
        )
    // もし、ルーティングにエラーがあれば、ログを吐く
    if err != nil {
        log.Fatal(err)
    }
    api.SetApp(router)
    log.Fatal(http.ListenAndServe(":8080", api.MakeHandler()))
}

type Impl struct {
    DB *gorm.DB
}

// DBから取得したデータを格納するための構造体を定義
type Comic struct {
    ID                 int64  `json:"id"`
    IsbnCode           int64  `json:"isbn_code"`
    Title              string `json:"title"`
    Author             string `json:"author"`
    Publisher          string `json:"publisher"`
    Cover              string `json:"cover"`
    ReleaseDate        string `json:"release_date"`
}

func (i *Impl) InitDB() {
    var err error
    // DBとの接続
    i.DB, err = gorm.Open("mysql", "{user_name}:{password}@tcp(localhost:3306)/{database_name}?parseTime=true&&loc=Asia%2FTokyo&charset=utf8")
    // parseTime=trueを書いておくと、なんかtime型をいい感じにしてくれるらしい
    // locをTokyoにしておくと、時間がズレないらしい
    // charsetをutf8で明示的に宣言しておくと、文字化けしなくてよろしいらしい
    if err != nil {
        log.Fatalf("Got error when connect database, the error is '%v'", err)
    }
    i.DB.LogMode(true)
}

func (i *Impl) GetAllComics(w rest.ResponseWriter, r *rest.Request) {
    // URLのQuery Parameterを取得
    // mapされます
    v := r.URL.Query()
    // ex) localhost:8080/api/comic/list?num=10&hoge=555 みたいなURLだったら
    // -> map[num:[10] hoge:[555]]となる。

    // mapからnumというキーの値を取り出す
    num := v.Get("num")
    // -> num = 10

    // Comic構造体の配列を宣言(インスタンス)
    comics := []Comic{}
    // i.DB.Find(&comics)とすると、全部のレコードが出てくる
    // Limit(num)を挟むと、出てくるレコード数を指定できる
    i.DB.Limit(num).Find(&comics)
    // 他の処理を書くときはこのあたりをいじると、別の事ができるようになる

    // エラーハンドリングしてないので怒られそう
    // 特にエラーがなく、正常のレスポンスをができていることをヘッダーに書いておく
    // http.StatusOK = 200
    w.WriteHeader(http.StatusOK)
    // 取得したデータをJSON形式で書いてレスポンスを返す
    w.WriteJson(&comics)
}

分かりやすいかどうかはさておき、コメントをたくさんつけておいたので、何かの参考になれば幸いです。

最後に

まだまだポンコツエンジニアなので、間違いや、コードがよろしくなかったりすると思うので、ぜひ、ご指摘下さい。
ありがとうございます。

hirokiseiya
20卒の大学生です. へっぽこYouTuberです. https://www.youtube.com/channel/UCYmnj1B3J_uNSCdSU9wXCrw
https://hirokiseiya.hatenablog.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away