「Golang」 ×「Gorm」でシンプルに「Mysql」を操作する

「`REST API`サーバから受け取ったリクエストパラメータの内容を元にリレーショナルデータベース（`Mysql`）を更新する」という流れで、説明を進めています。

以下の記事の続編という形で投稿を行っておりますので、仕様などの前提条件はこちらで確認していただけるとありがたいです。

> [「Golang」 ×「gorilla/mux router」でシンプルなREST APIサーバーを開発する](https://qiita.com/stranger1989/items/7d95778d26d34fd1ddef)

# <font color="LightSeaGreen">データベース（Mysql）準備</font>

コードを実装する前に、`Mysql`上でデータベースの作成を行います。
※ `Mysql`自体導入していない場合は、インストールが必要です。詳細は他のサイトで解説されているのでここでは割愛します。

```shell
# ルートユーザーでログイン
mysql -uroot

# mysql shell上
CREATE DATABASE { データベース名 };

# ログインユーザー作成
GRANT ALL PRIVILEGES ON { データベース名 }.* TO 'username'@'localhost' IDENTIFIED BY 'password';

# 一度mysqlからログアウトして、再度作成したユーザーでログイン
mysql -u username -p

# 念の為、データベースが作成されているか確認
SHOW DATABASES;
```

# <font color="LightSeaGreen">コード実装</font>

## バージョン情報

- go 1.14
- [github.com/jinzhu/gorm v1.9.12](https://github.com/jinzhu/gorm)
- gopkg.in/ini.v1 v1.56.0

## ORMマッピング

`SQL`文でデータベースを操作することも可能ですが、今回は「コードを簡素化する」・「単純なDB操作のみ行う」ために`ORM`でマッピングします。
データベースの複雑な操作が必要な場合は`SQL`をそのまま書いたほうが、メンテナンスしやすいと思います。
`Gorm`を採用した理由としては「スター数が多い」「ドキュメントが充実している」からです。

```shell:root上でgoモジュールインストール
go get github.com/jinzhu/gorm
```


## フォルダ構成

```:フォルダ構成
merchandise_control_system
├── config
│   └── config.go
├── controllers
│   └── webserver.go
├── models
│   └── middleware.go
├── config.ini
├── go.mod
├── go.sum
└── main.go
```

## サーバポート情報・データベース接続情報をconfigに記載する
APIサーバのポート番号、DBの接続情報は一箇所にまとめ読み込むようにすることで可読性が向上します。

```shell:root上でgoモジュールインストール
go get gopkg.in/ini.v1
```

```:config.ini
[db]
db_driver_name = mysql
db_name = { データベース名 }
db_user_name = { データベースユーザー名 }
db_user_password = { データベースパスワード }
db_host = 127.0.0.1
db_port = 3306

[api]
server_port = 8080
```

```golang:config/config.go
package config

import (
	"gopkg.in/ini.v1"
	"log"
	"os"
)

type ConfigList struct {
	DbDriverName   string
	DbName         string
	DbUserName     string
	DbUserPassword string
	DbHost         string
	DbPort         string
	ServerPort     int
}

var Config ConfigList

func init() {
	cfg, err := ini.Load("config.ini")
	if err != nil {
		log.Printf("Failed to read file: %v", err)
		os.Exit(1)
	}

	Config = ConfigList{
		DbDriverName:   cfg.Section("db").Key("db_driver_name").String(),
		DbName:         cfg.Section("db").Key("db_name").String(),
		DbUserName:     cfg.Section("db").Key("db_user_name").String(),
		DbUserPassword: cfg.Section("db").Key("db_user_password").String(),
		DbHost:         cfg.Section("db").Key("db_host").String(),
		DbPort:         cfg.Section("db").Key("db_port").String(),
		ServerPort:     cfg.Section("api").Key("server_port").MustInt(),
	}
}
```

## コード全体

イメージとしては、`main.go`からコントローラを呼び出し、APIサーバを起動。
APIリクエストが行われると、コントローラ内からモデルが呼び出され、データベースの値を参照・更新。
最終的にレスポンスが返されるといった流れになります。

```golang:main.go
package main

import (
	"merchandise_control_system/controllers"
)

func main() {
	controllers.StartWebServer()
}
```

```controllers/webserver.go
package controllers

import (
	"encoding/json"
	"fmt"
	"github.com/gorilla/mux"
	"io/ioutil"
	"log"
	"merchandise_control_system/config"
	"merchandise_control_system/models"
	"net/http"
	"strconv"
)

type DeleteResponse struct {
	Id string `json:"id"`
}

func rootPage(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Welcome to the Go Api Server")
	fmt.Println("Root endpoint is hooked!")
}

func fetchAllItems(w http.ResponseWriter, r *http.Request) {
	var items []models.Item
	// modelの呼び出し
	models.GetAllItems(&items)
	responseBody, err := json.Marshal(items)
	if err != nil {
		log.Fatal(err)
	}

	w.Header().Set("Content-Type", "application/json")
	w.Write(responseBody)
}

func fetchSingleItem(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id := vars["id"]

	var item models.Item
	// modelの呼び出し
	models.GetSingleItem(&item, id)
	responseBody, err := json.Marshal(item)
	if err != nil {
		log.Fatal(err)
	}

	w.Header().Set("Content-Type", "application/json")
	w.Write(responseBody)
}

func createItem(w http.ResponseWriter, r *http.Request) {
	reqBody, _ := ioutil.ReadAll(r.Body)

	var item models.Item
	if err := json.Unmarshal(reqBody, &item); err != nil {
		log.Fatal(err)
	}
	// modelの呼び出し
	models.InsertItem(&item)
	responseBody, err := json.Marshal(item)
	if err != nil {
		log.Fatal(err)
	}

	w.Header().Set("Content-Type", "application/json")
	w.Write(responseBody)
}

func deleteItem(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id := vars["id"]

	// modelの呼び出し
	models.DeleteItem(id)
	responseBody, err := json.Marshal(DeleteResponse{Id: id})
	if err != nil {
		log.Fatal(err)
	}

	w.Header().Set("Content-Type", "application/json")
	w.Write(responseBody)
}

func updateItem(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id := vars["id"]
	reqBody, _ := ioutil.ReadAll(r.Body)

	var updateItem models.Item
	if err := json.Unmarshal(reqBody, &updateItem); err != nil {
		log.Fatal(err)
	}
	// modelの呼び出し
	models.UpdateItem(&updateItem, id)
	convertUintId, _ := strconv.ParseUint(id, 10, 64)
	updateItem.Model.ID = uint(convertUintId)
	responseBody, err := json.Marshal(updateItem)
	if err != nil {
		log.Fatal(err)
	}

	w.Header().Set("Content-Type", "application/json")
	w.Write(responseBody)
}

func StartWebServer() error {
	fmt.Println("Rest API with Mux Routers")
	router := mux.NewRouter().StrictSlash(true)

	router.HandleFunc("/", rootPage)
	router.HandleFunc("/items", fetchAllItems).Methods("GET")
	router.HandleFunc("/item/{id}", fetchSingleItem).Methods("GET")

	router.HandleFunc("/item", createItem).Methods("POST")
	router.HandleFunc("/item/{id}", deleteItem).Methods("DELETE")
	router.HandleFunc("/item/{id}", updateItem).Methods("PUT")

	return http.ListenAndServe(fmt.Sprintf(":%d", config.Config.ServerPort), router)
}

```

```golang:models/middleware.go
package models

import (
	"fmt"
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
	"log"
	"merchandise_control_system/config"
	"time"
)

type Model struct {
	ID        uint        `gorm:"primary_key" json:"id"`
	CreatedAt *time.Time  `json:"created_at"`
	UpdatedAt *time.Time  `json:"updated_at"`
	DeletedAt *time.Time  `json:"deleted_at"`
}

type Item struct {
	Model
	JanCode      string     `gorm:"size:255" json:"jan_code,omitempty"`
	ItemName     string     `gorm:"size:255" json:"item_name,omitempty"`
	Price        int        `json:"price,omitempty"`
	CategoryId   int        `json:"category_id,omitempty"`
	SeriesId     int        `json:"series_id,omitempty"`
	Stock        int        `json:"stock,omitempty"`
	Discontinued bool       `json:"discontinued"`
	ReleaseDate  *time.Time `json:"release_date,omitempty"`
}

var Db *gorm.DB

func GetAllItems(items *[]Item) {
	Db.Find(&items)
}

func GetSingleItem(item *Item, key string) {
	Db.First(&item, key)
}

func InsertItem(item *Item) {
	Db.NewRecord(item)
	Db.Create(&item)
}

func DeleteItem(key string) {
	Db.Where("id = ?", key).Delete(&Item{})
}

func UpdateItem(item *Item, key string) {
	Db.Model(&item).Where("id = ?", key).Updates(
		map[string]interface{}{
			"jan_code":     item.JanCode,
			"item_name":    item.ItemName,
			"price":        item.Price,
			"category_id":  item.CategoryId,
			"series_id":    item.SeriesId,
			"stock":        item.Stock,
			"discontinued": item.Discontinued,
			"release_date": item.ReleaseDate,
		})
}

// データベースの初期化
func init() {
	var err error
	dbConnectInfo := fmt.Sprintf(
		`%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local`,
		config.Config.DbUserName,
		config.Config.DbUserPassword,
		config.Config.DbHost,
		config.Config.DbPort,
		config.Config.DbName,
	)

	// configから読み込んだ情報を元に、データベースに接続します
	Db, err = gorm.Open(config.Config.DbDriverName, dbConnectInfo)
	if err != nil {
		log.Fatalln(err)
	} else {
		fmt.Println("Successfully connect database..")
	}

	// 接続したデータベースにitemsテーブルを作成します
	Db.Set("gorm:table_options", "ENGINE = InnoDB").AutoMigrate(&Item{})
	if err != nil {
		fmt.Println(err.Error())
	} else {
		fmt.Println("Successfully created table..")
	}
}

```

# <font color="LightSeaGreen">デモ</font>

APIサーバを起動します。と同時にデータベース接続、テーブル作成が行われます。

```shell
go run main.go
```
Postmanからリクエストを送ってみます。（curlコマンドでもOKです）。

### 【POST】 /item
最初はデータベースが空のため、データを作成します。

```json:リクエストパラメータ
{
    "jan_code": "32739028488888",
    "item_name": "item_1",
    "price": 1000,
    "category_id": 1,
    "series_id": 1,
    "stock": 1000,
    "discontinued": true,
    "release_date": "2020-05-31T07:00:00.660666+09:00"
}
```

```json:レスポンスパラメータ
{
    "id": 1,
    "created_at": "2020-05-31T13:46:09.932124+09:00",
    "updated_at": "2020-05-31T13:46:09.932124+09:00",
    "deleted_at": null,
    "jan_code": "32739028488888",
    "item_name": "item_1",
    "price": 1000,
    "category_id": 1,
    "series_id": 1,
    "stock": 1000,
    "discontinued": true,
    "release_date": "2020-05-31T07:00:00.660666+09:00"
}
```

```:DB（Mysql）
select * from items;

+----+---------------------+---------------------+------------+----------------+-----------+-------+-------------+-----------+-------+--------------+---------------------+
| id | created_at          | updated_at          | deleted_at | jan_code       | item_name | price | category_id | series_id | stock | discontinued | release_date        |
+----+---------------------+---------------------+------------+----------------+-----------+-------+-------------+-----------+-------+--------------+---------------------+
|  1 | 2020-05-31 13:46:10 | 2020-05-31 13:46:10 | NULL       | 32739028488888 | item_1    |  1000 |           1 |         1 |  1000 |            1 | 2020-05-31 07:00:01 |
+----+---------------------+---------------------+------------+----------------+-----------+-------+-------------+-----------+-------+--------------+---------------------+
1 row in set (0.00 sec)

```

### 【GET】 /item/1
```json:レスポンスパラメータ
{
    "id": 1,
    "created_at": "2020-05-31T13:46:10+09:00",
    "updated_at": "2020-05-31T13:46:10+09:00",
    "deleted_at": null,
    "jan_code": "32739028488888",
    "item_name": "item_1",
    "price": 1000,
    "category_id": 1,
    "series_id": 1,
    "stock": 1000,
    "discontinued": true,
    "release_date": "2020-05-31T07:00:01+09:00"
}
```

### 【GET】 /items

```json:レスポンスパラメータ
[
    {
        "id": 1,
        "created_at": "2020-05-31T13:46:10+09:00",
        "updated_at": "2020-05-31T13:46:10+09:00",
        "deleted_at": null,
        "jan_code": "32739028488888",
        "item_name": "item_1",
        "price": 1000,
        "category_id": 1,
        "series_id": 1,
        "stock": 1000,
        "discontinued": true,
        "release_date": "2020-05-31T07:00:01+09:00"
    }
]
```

### 【PUT】 /item/1

```json:リクエストパラメータ
{
    "jan_code": "3273902878656",
    "item_name": "item_1_update",
    "price": 12000,
    "category_id": 1,
    "series_id": 1,
    "stock": 10,
    "discontinued": false,
    "release_date": "2020-05-30T10:30:19.978603+09:00"
}
```

```json:レスポンスパラメータ
{
    "id": 1,
    "created_at": null,
    "updated_at": "2020-05-31T14:26:04.165559+09:00",
    "deleted_at": null,
    "jan_code": "3273902878656",
    "item_name": "item_1_update",
    "price": 12000,
    "category_id": 1,
    "series_id": 1,
    "stock": 10,
    "discontinued": false,
    "release_date": "2020-05-30T10:30:19.978603+09:00"
}
```
```:DB（Mysql）
select * from items;

+----+---------------------+---------------------+------------+---------------+---------------+-------+-------------+-----------+-------+--------------+---------------------+
| id | created_at          | updated_at          | deleted_at | jan_code      | item_name     | price | category_id | series_id | stock | discontinued | release_date        |
+----+---------------------+---------------------+------------+---------------+---------------+-------+-------------+-----------+-------+--------------+---------------------+
|  1 | 2020-05-31 14:25:15 | 2020-05-31 14:26:04 | NULL       | 3273902878656 | item_1_update | 12000 |           1 |         1 |    10 |            0 | 2020-05-30 10:30:20 |
+----+---------------------+---------------------+------------+---------------+---------------+-------+-------------+-----------+-------+--------------+---------------------+
1 row in set (0.00 sec)

```

### 【DELETE】 /item/1

```json:レスポンスパラメータ
{
    "id": "1"
}
```

# <font color="LightSeaGreen">まとめ</font>

`Gorm`を使うと、比較的簡単にデータベース接続、テーブル作成、テーブル操作を行うことができました。
また、`ORM`の特徴として、DBを他のDB（例えば`PostgreSQL`）に変更したとしてもコードの変更が少なくて済むのもメリットかと思います。

細かいコードメンテナンスは必要ですが、一旦最低限の「APIサーバ起動」と「DB接続」が可能になりました。
今後こちらのサーバサイドコードを成長させながら、「ビュー」部分を`ReactNative`で実装していきたいと思います。