はじめに
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で終わるテーマパークを参考にしたデータを使うことにします
/* 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);
/* 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関連のファイルを殴り書きします
# 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
# mysqlバージョン
FROM mysql:latest
# confファイルをコピー
COPY mysqld_charset.cnf /etc/mysql/conf.d/mysql_charset.cnf
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ファイルに記述します
MYSQL_DBNAME = usk
MYSQL_USER = root
MYSQL_PASSWORD = admin
MYSQL_PROTCOL = tcp(USK_DB:3306)
golang
modやsumファイルは各自好きな名前で設定してください
cmdフォルダ
特になんの変哲もない実行用のファイルです
package main
import (
"go/router"
)
func main() {
router.Init()
}
libフォルダ
mysql上のデータをgolangで扱えるようにするライブラリgormを使って書いていきます
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テーブル
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テーブル
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テーブルを結合する
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フォルダ
ルーティング時に実行する関数を記述します
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)
}
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フォルダ
ルーティング先とそこで実行する関数を記述していきます
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で確認すると
GET http://localhost:8080/area?ar=ヘリー
こんな感じで撮れます
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": "ヘリーポッター"
}
]
}
]
最後に
ここまで実装するのに結構時間がかかりました〜
特にデータを入れ子構造にする方法がわかんなさすぎてめっちゃ苦労しました