[golang]gormを使ってローカル環境のMySQLとCRUD操作


go言語を勉強してだいたい一週間経ちました。最初の3、4日間は書籍を見ながら文法の勉強をしていましたが、今はドキュメントとパッケージのドキュメントやGitHub、Qiitaを見ながら実践してます。<br>

## 概要

今回はローカル環境にインストールしているMySQLとの接続とCRUD操作を実践してみます。<br>

### 環境

 -  go1.11.4 darwin/amd64
 -  mysql  Ver 8.0.12
 -  curl 7.54.0

### 参考にしたもの

 - https://github.com/ant0ine/go-json-rest
 - http://gorm.io/docs/connecting_to_the_database.html
 - [MySQLインストール時にやること（DBとユーザーの作成等）](https://qiita.com/daichi87gi/items/f9dac6cd8acc3ad4330d)

## この記事でやること

 1. パッケージのインストール
 2. MySQLの権限付きユーザの生成
 3. goプログラム作成
   - RESTfullなAPIサーバ
   - CRUD操作
   - MySQLとの接続
 4. サーバーの起動
 5. curlでの動作確認

## パッケージのインストール

まずはMySQLドライバーと、GoのORMのパッケージインストール<br>

`go-json-rest`がない方はこちらもインストール<br>

```shell

$ go get github.com/go-sql-driver/mysql

$ go get github.com/jinzhu/gorm

$ go get github.com/ant0ine/go-json-rest/rest
```

## MySQLの権限付きユーザの生成

```shell
$ mysql -u root -p
```
MySQL内で以下の設定を行う。Host以外、ユーザ名とパスワードはそれぞれ任意でお願いします。<br>

```sql
-- データベース名:country
CREATE DATABASE country;

-- ユーザ名:gorm パスワード:passwordで作成。 Hostは'localhost'で設定する。
CREATE USER 'gorm'@'localhost' IDENTIFIED BY 'password';

-- ユーザ名:gorm に DB:country以下のテーブルも含めて権限を与える。
GRANT ALL PRIVILEGES ON country.* TO 'gorm'@'localhost';

```

権限が委譲できているか確認してみましょう。

```sql

SHOW GRANTS FOR 'gorm'@'localhost';

+-----------------------------------------------------------+
| Grants for gorm@localhost                                 |
+-----------------------------------------------------------+
| GRANT USAGE ON *.* TO `gorm`@`localhost`                  |
| GRANT ALL PRIVILEGES ON `country`.* TO `gorm`@`localhost` |
+-----------------------------------------------------------+
2 rows in set (0.00 sec)
```

### goプログラムの作成

```golang:main.go
package main

import (
	"log"
	"net/http"
	"time"

	"github.com/ant0ine/go-json-rest/rest"
	_ "github.com/go-sql-driver/mysql" // エイリアスでprefixを省略できる
	"github.com/jinzhu/gorm"
)

type Country struct {
	Id        int64     `json:"id"`
	Name      string    `sql:"size:1024" json:"name"`
	CreatedAt time.Time `json:"createdAt"`
}

type Impl struct {
	DB *gorm.DB
}

func (i *Impl) InitDB() {
	var err error
	// MySQLとの接続。ユーザ名：gorm パスワード：password DB名：country
	i.DB, err = gorm.Open("mysql", "gorm:password@/country?charset=utf8&parseTime=True&loc=Local")
	if err != nil {
		log.Fatalf("Got error when connect database, the error is '%v'", err)
	}
	i.DB.LogMode(true)
}

// DBマイグレーション
func (i *Impl) InitSchema() {
	i.DB.AutoMigrate(&Country{})
}

func main() {

	i := Impl{}
	i.InitDB()
	i.InitSchema()

	api := rest.NewApi()
	api.Use(rest.DefaultDevStack...)
	router, err := rest.MakeRouter(
		rest.Get("/countries", i.GetAllCountries),
		rest.Post("/countries", i.PostCountry),
		rest.Get("/countries/:id", i.GetCountry),
		rest.Put("/countries/:id", i.PutCountry),
		rest.Delete("/countries/:id", i.DeleteCountry),
	)
	if err != nil {
		log.Fatal(err)
	}

	log.Printf("server started.")
	api.SetApp(router)
	log.Fatal(http.ListenAndServe(":8080", api.MakeHandler()))
}

// countriesテーブル内のデータ全出力
func (i *Impl) GetAllCountries(w rest.ResponseWriter, r *rest.Request) {
	countries := []Country{}
	i.DB.Find(&countries)
	w.WriteJson(&countries)
}

// パスパラメータ:idの国の該当データを出力
func (i *Impl) GetCountry(w rest.ResponseWriter, r *rest.Request) {
	id := r.PathParam("id")
	country := Country{}
	if i.DB.Find(&country, id).Error != nil {
		rest.NotFound(w, r)
		return
	}
	w.WriteJson(&country)
}

// json形式のデータをPOST {name:国名}
func (i *Impl) PostCountry(w rest.ResponseWriter, r *rest.Request) {
	country := Country{}
	err := r.DecodeJsonPayload(&country)
	if err != nil {
		rest.Error(w, err.Error(), http.StatusInternalServerError)
	}
	err = i.DB.Save(&country).Error
	if err != nil {
		rest.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.WriteJson(&country)
}

// パスパラメータ:idの国の該当データのNameを変更し出力
func (i *Impl) PutCountry(w rest.ResponseWriter, r *rest.Request) {

	id := r.PathParam("id")
	country := Country{}
	if i.DB.First(&country, id).Error != nil {
		rest.NotFound(w, r)
		return
	}
	updated := Country{}
	if err := r.DecodeJsonPayload(&updated); err != nil {
		rest.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	country.Name = updated.Name

	if err := i.DB.Save(&country).Error; err != nil {
		rest.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.WriteJson(&country)
}

// パスパラメータ:idの国の該当データを削除
func (i *Impl) DeleteCountry(w rest.ResponseWriter, r *rest.Request) {
	id := r.PathParam("id")
	country := Country{}
	if i.DB.First(&country, id).Error != nil {
		rest.NotFound(w, r)
		return
	}
	if err := i.DB.Delete(&country).Error; err != nil {
		rest.Error(w, err.Error(), http.StatusInsufficientStorage)
		return
	}
	w.WriteHeader(http.StatusOK)
}

```

## サーバの起動

ターミナルでサーバを起動させます。

```shell
go run main.go
```

以下の内容が出力されていればサーバが起動しています。
解除したい場合は`[Ctr] + [c]`

```shell
[2019-01-25 15:56:32]  [93.30ms]  CREATE TABLE `countries` (`id` bigint AUTO_INCREMENT,`name` varchar(1024),`created_at` timestamp NULL , PRIMARY KEY (`id`))
[0 rows affected or returned ]
2019/01/25 15:56:32 server started.
```

マイグレーションは構造体内を分析してTable作られてるみたい。
サーバの再起動時、MySQL内に`Countriesテーブル`があれば再度マイグレーションされることはありません。便利ですね〜。<br>

## curlでの動作確認

APIサーバの起動ができたら、__別のターミナルを起動し__curlで動作確認をしてみましょう。

### 投稿

```shell
$ curl -i -H 'Content-Type:application/json' -d '{"name":"JAPAN"}' http://localhost:8080/countries
```
ステータスコード200とレスポンスが返ってきたら成功です。

```shell
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Powered-By: go-json-rest
Date: Fri, 25 Jan 2019 05:43:05 GMT
Content-Length: 86

{
  "id": 1,
  "country": "JAPAN",
  "createdAt": "2019-01-25T14:43:05.110759+09:00"
}
```

以下のコマンド、色々試してみてください。

### 全表示（index）

```shell
curl -i http://localhost:8080/countries
```

### 編集

```shell
curl -i -X PUT -H 'Content-Type:application/json' -d '{"name":"Japan"}' http://localhost:8080/countries/1
```

### 削除

```shell
curl -i -X DELETE http://localhost:8080/countries/2
```

### 感想

フレームワーク（Laravel）を経験しているので、ルーターやコントローラーの動きが理解しやすい。
また、モデルは構造体に相当している。ORMもLaravelと同じ。マイグレーションも便利ですね〜。