Docker + gorm + gin で入れ子構造のAPIを作る

# はじめに
STECH / 愛知工業大学 システム工学研究会 共同企画 Advent Calendar 2022 の8日目の記事を書かせていただきます水谷です！
あるイベントでGoとmySQLとDockerでAPIサーバを作ったのでその時のことをまとめました。
もしおかしな点があったら指摘していただけると助かります！

# ファイル構成

```
${ROOT}
├── docker
│   ├── go
│   │   └── Dockerfile
│   └── mysql
│       ├── Dockerfile
│       ├── init
│       │   ├── 1-area.sql
│       │   └── 2-institution.sql
│       └── mysqld_charset.cnf
├── docker-compose.yml
├── go
│   ├── cmd
│   │   └── main.go
│   ├── controller
│   │   ├── area_controller.go
│   │   └── institution_controller.go
│   ├── go.mod
│   ├── go.sum
│   ├── lib
│   │   └── sql_handler.go
│   ├── model
│   │   ├── area.go
│   │   ├── combine.go
│   │   └── institute.go
│   └── router
│       └── router.go
└── sample.http
```

# 今回扱うデータについて
今回はUから始まりJで終わるテーマパークを参考にしたデータを使うことにします
```sql:1-area.sql
/* CREATE DATABASE */
CREATE DATABASE usk;

USE usk
/* CREATE TABLE */
CREATE TABLE IF NOT EXISTS areas(
    area_name VARCHAR(100),
    latitude DECIMAL(10, 2),
    longitude DECIMAL(10, 2),
    primary key (area_name)
) ENGINE = InnoDB;

/* INSERT QUERY */
INSERT INTO
    areas(area_name, latitude, longitude)
VALUES
    ('ヘリーポッター', 34.66889594382906, 135.43168415339426);

INSERT INTO
    areas(area_name, latitude, longitude)
VALUES
    ('スーパーモリオワールド', 34.66781671914631, 135.43048988193001);
```

```sql:2-institution.sql
/* CREATE TABLE */
CREATE TABLE IF NOT EXISTS usk.institutions(
    id int not NULL auto_increment,
    institution_name VARCHAR(100),
    area_name VARCHAR(100),
    primary key (id),
    foreign key (area_name) references usk.areas(area_name)
) ENGINE = InnoDB;

/* INSERT QUERY */
INSERT INTO
    usk.institutions(institution_name, area_name)
VALUES
    ('モリオモーターズ', 'スーパーモリオワールド');

INSERT INTO
    usk.institutions(institution_name, area_name)
VALUES
    ('へリーポッタザーライド', 'ヘリーポッター');

INSERT INTO
    usk.institutions(institution_name, area_name)
VALUES
    ('モリオカート', 'スーパーモリオワールド');

INSERT INTO
    usk.institutions(institution_name, area_name)
VALUES
    ('ぽっくんフラワーポニック', 'スーパーモリオワールド');

INSERT INTO
    usk.institutions(institution_name, area_name)
VALUES
    ('へリーポッターのトイレ', 'ヘリーポッター');

INSERT INTO
    usk.institutions(institution_name, area_name)
VALUES
    ('へリーポッターの売店', 'ヘリーポッター');
```



# コード
## Docker関連ファイル
まずはDocker関連のファイルを殴り書きします
```dockerfile:Dockerfile (./docker/go)
# goバージョン
FROM golang:1.19.3-alpine
# アップデートとgitのインストール
RUN apk add --update &&  apk add git
# appディレクトリの作成
RUN mkdir /go/src/app
# ワーキングディレクトリの設定
WORKDIR /go/src/app
# ホストのファイルをコンテナの作業ディレクトリに移行
ADD . /go/src/app
```

```dockerfile:Dockerfile (./docker/mysql)
# mysqlバージョン
FROM mysql:latest

# confファイルをコピー
COPY mysqld_charset.cnf /etc/mysql/conf.d/mysql_charset.cnf
```

```docker:docker-compose.yml
version: "3"
services:
  go:
    container_name: USK_GO
    build:
      context: ./docker/go
      dockerfile: Dockerfile
    stdin_open: true
    tty: true
    env_file:
      - ./Docker/go/.env
    volumes:
      - ./go:/go/src/app
    ports:
      - 8080:8080
    depends_on:
      - "mysql"

  mysql:
    container_name: USK_DB
    build:
      context: ./docker/mysql
      dockerfile: Dockerfile
    ports:
      - "3306:3306"
    volumes:
      - ./docker/mysql/init:/docker-entrypoint-initdb.d
    environment:
      MYSQL_ROOT_PASSWORD: abcdefj
```

## 環境変数
gormでDocker上のmysqlに接続するための値をenvファイルに記述します
```env:.env
MYSQL_DBNAME = usk
MYSQL_USER = root
MYSQL_PASSWORD = admin
MYSQL_PROTCOL = tcp(USK_DB:3306)
```


## golang
modやsumファイルは各自好きな名前で設定してください
### cmdフォルダ
特になんの変哲もない実行用のファイルです
```golang:main.go
package main

import (
	"go/router"
)

func main() {
	router.Init()
}
```

### libフォルダ
mysql上のデータをgolangで扱えるようにするライブラリgormを使って書いていきます
```golang:sql_handler.go
package lib

import (
	"fmt"
	"os"
	"time"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func SqlConnect() (database *gorm.DB) {
	USER := os.Getenv("MYSQL_USER")
	PASS := os.Getenv("MYSQL_PASSWORD")
	PROTOCOL := os.Getenv("MYSQL_PROTCOL")
	DBNAME := os.Getenv("MYSQL_DBNAME")
	
	dsn := USER + ":" + PASS + "@" + PROTOCOL + "/" + DBNAME + "?charset=utf8&parseTime=true&loc=Asia%2FTokyo"
	dialector := mysql.Open(dsn)

	var db *gorm.DB
	var err error

	if db, err = gorm.Open(dialector); err != nil {
		connect(db, dialector, 100)
	}
	fmt.Println("db connected!!")

	return db
}

func connect(db *gorm.DB, dialector gorm.Dialector, count uint) {
	var err error
	if db, err = gorm.Open(dialector); err != nil {
		if count > 1 {
			time.Sleep(time.Second * 2)
			count--
			fmt.Printf("retry... count:%v\n", count)
			connect(db, dialector, count)
			return
		}
		panic(err.Error())
	}
}
```
connect関数はgormでmysqlに接続する際にまだサーバが立ち上がっていない場合があるのでそれを避けるために実装してあります


### modelフォルダ
扱うデータの構造体を定義していきます

areaテーブル
```golang:area.go
package model

// Area テーブル情報
type Area struct {
	AreaName string      `gorm:"primarykey:AreaName"`
	Latitude     string
	Longitude    string
	Institutions   []Institution `gorm:"foreignkey:AreaName"`
}

// エリア名を検索してデータを取得
func GetArea(area_name string) []*Area {
	result := []*Area{}

	// area_nameが空文字だった時
	if area_name == "" {
		return result
	}
	
	db.Where("area_name LIKE ?", "%"+area_name+"%").Find(&result)
	result = CombineArea(result)
	return result
}
```

instituteテーブル
```golang:institute.go
package model

// Institution テーブル情報
type Institution struct {
	ID int
	InstitutionName string
	AreaName string
}

// 施設名を検索してデータを取得
func GetRoomInstitutionName(institution_name string) []*Area {
	institution := []*Institution{}
	result := []*Area{}

	// institution_nameが空文字だった時
	if institution_name == "" {
		return result
	}

	db.Where("institution_name LIKE ?", "%"+institution_name+"%").Find(&institution)
	result = CombineInstitution(institution)
	return result
}
```

area instituteテーブルを結合する
```golang:combine.go
package model

import (
	"go/lib"
)

var db = lib.SqlConnect()

// Areaテーブル検索の時テーブルを結合
func CombineArea(area []*Area) []*Area {
	// 構造体の定義
	institution := []*Institution{}

	// DBのデータを構造体の配列に格納
	db.Find(&institution)

	// AreaのInstitutionに構造体を入れる
	for _, a := range area {
		for _, i := range institution {
			if a.AreaName == i.AreaName {
				a.Institutions = append(a.Institutions, *i)
			}
		}
	}
	return area
}

// Institutionテーブル検索の時テーブルを結合
func CombineInstitution(institution []*Institution) []*Area {
	// 構造体の定義
	result := []*Area{}
	area := []*Area{}

	// DBのデータを構造体の配列に格納
	db.Find(&area)

	// AreaのInstitutionに構造体を入れる
	for _, a := range area {
		for _, i := range institution {
			if a.AreaName == i.AreaName {
				db.Where("area_name LIKE ?", "%"+a.AreaName+"%").Find(&result)
				for _, r := range result {
					r.Institutions = append(r.Institutions, *i)
				}
			}
		}
	}
	return result
}
```

## controllerフォルダ
ルーティング時に実行する関数を記述します
```golang:area_controller.go
package controller

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"go/model"
)

func GetSearchAreaResult(c *gin.Context) {
	ar := c.Query("ar")
	print(ar)
	result := model.GetArea(ar)
	c.JSON(http.StatusOK, result)
}
```

```golang:institute.go
package controller

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"go/model"
)

func GetSearchInstituteResult(c *gin.Context) {
	in := c.Query("in")
	result := model.GetRoomInstitutionName(in)
	c.JSON(http.StatusOK, result)
}
```
c.Queryで area?ar=xxxxx のxxxxxを取得する
そして、resultにGetRoomInstitutionNameで施設名を検索してそれを格納
最後にJSON形式でクライアント側に返す流れ

## routerフォルダ
ルーティング先とそこで実行する関数を記述していきます

```golang:router.go
package router

import (
	"github.com/gin-gonic/gin"
	"go/controller"
)

func Init() {
	r := gin.Default()
	r.GET("/area", controller.GetSearchAreaResult)
	r.GET("/institution", controller.GetSearchInstituteResult)
	r.Run()
}
```

# 実行方法
docker-compose.ymlがある場所で
```
docker compose build
```
```
docker compose up
```

```
docker compose build
```
そしたらgoコンテナ入って
```
docker exec -it koukaten2022_GO ash
```
main.go ファイルがある cmd フォルダまで移動して
```
cd cmd
```
実行
```
go run main.go
```
こんな感じの画面が出たら成功
```
[GIN-debug] GET    /area                     --> go/controller.GetSearchAreaResult (3 handlers)
[GIN-debug] GET    /institution              --> go/controller.GetSearchInstituteResult (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
```
# 本当に値が取れるか確認
vscodeの拡張機能のREST Clientで確認すると

```sample.http
GET http://localhost:8080/area?ar=ヘリー
```
こんな感じで撮れます
```result
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Wed, 07 Dec 2022 06:45:41 GMT
Content-Length: 386
Connection: close

[
  {
    "AreaName": "ヘリーポッター",
    "Latitude": "34.67",
    "Longitude": "135.43",
    "Institutions": [
      {
        "ID": 2,
        "InstitutionName": "へリーポッタザーライド",
        "AreaName": "ヘリーポッター"
      },
      {
        "ID": 5,
        "InstitutionName": "へリーポッターのトイレ",
        "AreaName": "ヘリーポッター"
      },
      {
        "ID": 6,
        "InstitutionName": "へリーポッターの売店",
        "AreaName": "ヘリーポッター"
      }
    ]
  }
]
```

# 最後に
ここまで実装するのに結構時間がかかりました〜
特にデータを入れ子構造にする方法がわかんなさすぎてめっちゃ苦労しました