LoginSignup
5
4

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-12-07

はじめに

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で終わるテーマパークを参考にしたデータを使うことにします

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);
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 (./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 (./docker/mysql)
# mysqlバージョン
FROM mysql:latest

# confファイルをコピー
COPY mysqld_charset.cnf /etc/mysql/conf.d/mysql_charset.cnf
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
MYSQL_DBNAME = usk
MYSQL_USER = root
MYSQL_PASSWORD = admin
MYSQL_PROTCOL = tcp(USK_DB:3306)

golang

modやsumファイルは各自好きな名前で設定してください

cmdフォルダ

特になんの変哲もない実行用のファイルです

main.go
package main

import (
	"go/router"
)

func main() {
	router.Init()
}

libフォルダ

mysql上のデータをgolangで扱えるようにするライブラリgormを使って書いていきます

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テーブル

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テーブル

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テーブルを結合する

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フォルダ

ルーティング時に実行する関数を記述します

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)
}
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フォルダ

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

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=ヘリー

こんな感じで撮れます

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": "ヘリーポッター"
      }
    ]
  }
]

最後に

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

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4