8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Go 言語Advent Calendar 2023

Day 2

Go(Echo)+MySQLでCRUDを行えるAPIサーバをDocker上で構築する(2)APIサーバを構築する

Last updated at Posted at 2023-11-11

はじめに

Go(Echo)を用いたAPIサーバを構築したい方向けとなっております。
前回:Docker上でGo(Echo)+MySQLのAPIサーバを構築する(1)GoとMySQLを接続し、テーブルを作成する
この記事では、Go(Echo)を用いてAPIサーバを構築するまで説明します。
間違いや改善点などあればお気軽に仰っていただけると幸いです。

環境

Docker:v24.0.6
Docker Compose:v2.4.1

最終的な構成図

.
├── docker-compose.yml
├── golang
│   ├── Dockerfile
│   └── app
│       ├── handler
│       │   └── handler.go
│       ├── go.mod
│       ├── go.sum
│       ├── main.go
│       └── database
│           └── database.go
└── mysql
    └── Dockerfile

前回のおさらい

前回は、Docker Composeを用いてGoとMySQLを接続し、Userテーブルを作成する所まで行いました。
以下が、前回時点でのディレクトリ構成です。

.
├── docker-compose.yml
├── golang
│   ├── Dockerfile
│   └── app
│       ├── go.mod
│       ├── go.sum
│       └── main.go
└── mysql
    ├── Dockerfile
    └── .env

1. Echoを用いてサーバを起動する

前回main.goにDBの操作を記述していたのですが、ファイルを分けた方が可読性が向上するため新しくapp直下にdatabase/database.goを作成します。

database/database.go
package database

import (
	"errors"
	"fmt"
	"os"

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

// MySQLに作成するUserテーブルの定義
type User struct {
	// gorm.Modelをつけると、idとCreatedAtとUpdatedAtとDeletedAtが作られる
	gorm.Model

	Name string
	Age  int
}

// 外部参照可能なDB変数を定義
var Db *gorm.DB

// dbInit()が呼び出されたときに最初に処理される関数
func init() {
	Db = dbInit()
	// Userテーブル作成
	Db.AutoMigrate(&User{})
}

// DBを起動させる
func dbInit() *gorm.DB {
	// [ユーザ名]:[パスワード]@tcp([ホスト名]:[ポート番号])/[データベース名]?charset=[文字コード]
	dsn := fmt.Sprintf(`%s:%s@tcp(db:3306)/%s?charset=utf8mb4&parseTime=True`, os.Getenv("MYSQL_USER"), os.Getenv("MYSQL_PASSWORD"), os.Getenv("MYSQL_DATABASE"))
	// DBへの接続を行う
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

	// エラーが発生した場合、エラー内容を表示
	if err != nil {
		fmt.Println(err)
	}
	// 接続に成功した場合、「db connected!!」と表示する
	fmt.Println("db connected!!")
	return db
}

次に、main.goを編集します。database.goに移行した部分は削除し、Echoを用いて簡単なサーバを構築します。

main.go
package main

import (
    "net/http"

	"github.com/labstack/echo"
	"github.com/labstack/echo/middleware"
)

func main() {
    // Echoインスタンスを作成
    e := echo.New()

    // httpリクエストの情報をログに表示
	e.Use(middleware.Logger())
	// パニックを回復し、スタックトレースを表示
	e.Use(middleware.Recover())

    // ルートを設定(第一引数にエンドポイント、第二引数にハンドラーを指定)
    e.GET("/", hello)

    // サーバーをポート番号8080で起動
    e.Logger.Fatal(e.Start(":8080"))
}

// ハンドラーを定義(どういう処理を実行するか)
func hello(c echo.Context) error {
  return c.String(http.StatusOK, "Hello, World!")
}

この例では、http://localhost:8080にアクセスすると「Hello World!」という文字列が返ってきます。

2. ホットリロードライブラリ「Air」を導入する

現状ですと、ソースコードを変更する度にビルドする必要があるので、GoのホットリロードライブラリであるAirを導入していきます。
まず、golang/DockerfileでAirをインストールし、実行するように変更します。

golang/Dockerfile

# バージョン指定
FROM golang:1.19.2-alpine3.16
# go moduleモードに設定
ENV GO111MODULE on
# 必要なものをインストール
RUN apk update && \
    apk --no-cache add git && \
    apk add bash
# 作業ディレクトリ指定
WORKDIR /go/src/app
# go.mod作成
RUN go mod init app
# go.mod更新&Airインストール
RUN go mod tidy && \
    go install github.com/cosmtrek/air@v1.40.4
# Air起動
CMD ["air", "-c", ".air.toml"]

次に、main.goと同じ場所(今回はgolang/app)にAirの設定ファイルである.air.tomlを追加します。
.air.tomlの中身は、公式のテンプレートを使用しています。
https://github.com/cosmtrek/air/blob/master/air_example.toml

.air.toml
# Config file for [Air](https://github.com/cosmtrek/air) in TOML format

# Working directory
# . or absolute path, please note that the directories following must be under root.
root = "."
tmp_dir = "tmp"

[build]
# Array of commands to run before each build
pre_cmd = ["echo 'hello air' > pre_cmd.txt"]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./tmp/main ."
# Array of commands to run after ^C
post_cmd = ["echo 'hello air' > post_cmd.txt"]
# Binary file yields from `cmd`.
bin = "tmp/main"
# Customize binary, can setup environment variables when run your app.
full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
# Watch these filename extensions.
include_ext = ["go", "tpl", "tmpl", "html"]
# Ignore these filename extensions or directories.
exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules"]
# Watch these directories if you specified.
include_dir = []
# Watch these files.
include_file = []
# Exclude files.
exclude_file = []
# Exclude specific regular expressions.
exclude_regex = ["_test\\.go"]
# Exclude unchanged files.
exclude_unchanged = true
# Follow symlink for directories
follow_symlink = true
# This log file places in your tmp_dir.
log = "air.log"
# Poll files for changes instead of using fsnotify.
poll = false
# Poll interval (defaults to the minimum interval of 500ms).
poll_interval = 500 # ms
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 0 # ms
# Stop running old binary when build errors occur.
stop_on_error = true
# Send Interrupt signal before killing process (windows does not support this feature)
send_interrupt = false
# Delay after sending Interrupt signal
kill_delay = 500 # nanosecond
# Rerun binary or not
rerun = false
# Delay after each executions
rerun_delay = 500
# Add additional arguments when running binary (bin/full_bin). Will run './tmp/main hello world'.
args_bin = ["hello", "world"]

[log]
# Show log time
time = false
# Only show main log (silences watcher, build, runner)
main_only = false

[color]
# Customize each part's color. If no color found, use the raw app log.
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"

[misc]
# Delete tmp directory on exit
clean_on_exit = true

[screen]
clear_on_rebuild = true
keep_scroll = true

これで、Dockerコンテナを再起動しなくてもソースコードの変更が反映されるようになります。

3. GoのORMであるgormを用いてDBのCRUD操作が行えるようにする

次は、先ほど作成したdatabase.goに追記して、DBに対してGET、POST、PUT、DELETEを行う関数を定義します。

database.go
package database

import (
	"errors"
	"fmt"
	"os"

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

// MySQLに作成するUserテーブルの定義
type User struct {
	// gorm.Modelをつけると、idとCreatedAtとUpdatedAtとDeletedAtが作られる
	gorm.Model

	Name string
	Age  int
}

// 外部参照可能なDB変数を定義
var Db *gorm.DB

// dbInit()が呼び出されたときに最初に処理される関数
func init() {
	Db = dbInit()
	// Userテーブル作成
	Db.AutoMigrate(&User{})
}

// DBを起動させる
func dbInit() *gorm.DB {
	// [ユーザ名]:[パスワード]@tcp([ホスト名]:[ポート番号])/[データベース名]?charset=[文字コード]
	dsn := fmt.Sprintf(`%s:%s@tcp(db:3306)/%s?charset=utf8mb4&parseTime=True`,
            os.Getenv("MYSQL_USER"), os.Getenv("MYSQL_PASSWORD"), os.Getenv("MYSQL_DATABASE"))
	// DBへの接続を行う
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

	// エラーが発生した場合、エラー内容を表示
	if err != nil {
		fmt.Println(err)
	}
	// 接続に成功した場合、「db connected!!」と表示する
	fmt.Println("db connected!!")
	return db
}

// ここから追加

// レコード全件取得
func ReadAll(db *gorm.DB) ([]User, error) {
	// user構造体のスライスを作成
	users := []User{}
	// 全てのuser情報を取得
	result := db.Find(&users)
	// エラー発生時はエラー内容を表示
	if result.Error != nil {
		return users, result.Error
	}
	// 全てのuser情報を返す
	return users, result.Error
}

// レコード単体取得
func ReadOne(db *gorm.DB, id int) (User, error) {
	// user構造体を作成
	user := User{}
	// 特定のidのuser情報を取得
	result := db.First(&user, id)
	// idが見つからない場合はエラー内容を表示
	if errors.Is(result.Error, gorm.ErrRecordNotFound) {
		return user, result.Error
	}
	// 特定のuser情報を返す
	return user, result.Error
}

// レコード作成
func InsertUser(db *gorm.DB, name string, age int) error {
	// 引数を元に追加するuser構造体を作成
	user := User{
		Name: name,
		Age:  age,
	}
	// 新規userを作成する
	result := db.Create(&user)
	// エラー発生時はエラー内容を表示
	if result.Error != nil {
		return result.Error
	}

	return nil
}

// レコード更新
func UpdateUser(db *gorm.DB, id int, name string, age int) error {
	// 更新するユーザを取得する
	user, err := ReadOne(db, id)
	if err != nil {
		return err
	}
	// 構造体にidがある場合はupdateされる
	user.Name = name
	user.Age = age
	result := db.Save(&user)
	// エラー発生時はエラー内容を表示
	if result.Error != nil {
		return result.Error
	}

	return nil
}

// レコード削除
func DeleteUser(db *gorm.DB, id int) (User, error) {
	// 削除するuser情報を取得
	user, err := ReadOne(db, id)
	if err != nil {
		return user, err
	}
	// DeletedAtがある場合は論理削除になる
	db.Where("id = ?", id).Delete(&User{})
	// 物理削除の場合は以下のようになる
	// db.Unscoped().Where("id = ?", id).Delete(&User{})

	// 削除したuser情報を返す
	return user, err
}

こちらを元にハンドラーを作成し、エンドポイントにアクセスした際にこの関数からCRUD操作を行う想定です。

4. ハンドラーとルーティングを作成し、GoからCRUD操作を行う

エンドポイントにアクセスした際に呼び出されるハンドラーを定義します。
appディレクトリ直下にhandler/handler.goを追加し、以下のように記述します。

handler/handler.go
package handler

import (
	"app/database"
	"net/http"
	"strconv"

	"github.com/labstack/echo"
)

// 全てのuser情報を取得し、JSON形式で返す
func GetAllUser(c echo.Context) error {
	// 全てのuser情報を取得
	users, err := database.ReadAll(database.Db)
	if err != nil {
		return err
	}
	// 全てのuser情報をJSON形式で返す
	return c.JSON(http.StatusOK, users)
}

// 特定のuser情報を取得し、JSON形式で返す
func GetOneUser(c echo.Context) error {
	// パスパラメータ「id」を取得
	param := c.Param("id")
	// idを整数型に変換
	id, _ := strconv.Atoi(param)
	// 特定のuser情報を取得
	user, err := database.ReadOne(database.Db, id)
	if err != nil {
		return err
	}
	// 特定のuser情報をJSON形式で返す
	return c.JSON(http.StatusOK, user)
}

// 送られてきた情報を元に新規userを作成し、JSON形式で返す
func PostNewUser(c echo.Context) error {
	// user構造体作成
	u := new(database.User)
	// 送られてきた情報を元に構造体へのデータ格納を行う
	err := c.Bind(u)
	// エラー発生時はエラーを返す
	if err != nil {
		return err
	}
	// 構造体の情報を元に新規userを作成
	database.InsertUser(database.Db, u.Name, u.Age)
	// 新規user情報をJSON形式で返す
	return c.JSON(http.StatusOK, u)
}

// 送られてきた情報を元にuser情報を更新し、JSON形式で返す
func PutUser(c echo.Context) error {
	// パスパラメータ「id」を取得
	param := c.Param("id")
	// idを整数型に変換
	id, _ := strconv.Atoi(param)
	// user構造体作成
	u := new(database.User)
	// 送られてきた情報を元に構造体へのデータ格納を行う
	err := c.Bind(u)
	// エラー発生時はエラーを返す
	if err != nil {
		return err
	}
	// 構造体の情報を元にuserを更新
	database.UpdateUser(database.Db, id, u.Name, u.Age)
	// 更新したuser情報をJSON形式で返す
	return c.JSON(http.StatusOK, u)
}

// 送られてきた情報を元にuserを削除し、JSON形式で返す
func DeleteUser(c echo.Context) error {
	// パスパラメータ「id」を取得
	param := c.Param("id")
	// idを整数型に変換
	id, _ := strconv.Atoi(param)
	// 構造体の情報を元にuserを更新
	user, err := database.DeleteUser(database.Db, id)
	if err != nil {
		return err
	}
	// 更新したuser情報をJSON形式で返す
	return c.JSON(http.StatusOK, user)
}

このハンドラーを、main.goのルーティングを新たに作成する際に指定します。

main.go
package main

import (
	"app/handler"

	"github.com/labstack/echo"
	"github.com/labstack/echo/middleware"
)

func main() {
	// Echoインスタンスを作成
	e := echo.New()

	// ミドルウェアを設定
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())
 
    /* ここから追加 */
	// ルートを設定(第一引数にエンドポイント、第二引数にハンドラーを指定)
	e.GET("/users", handler.GetAllUser)
	e.POST("/users", handler.PostNewUser)
	e.GET("/users/:id", handler.GetOneUser)
	e.PUT("/users/:id", handler.PutUser)
	e.DELETE("/users/:id", handler.DeleteUser)
    /* ここまで */

	// サーバーをポート番号8080で起動
	e.Logger.Fatal(e.Start(":8080"))
}

4. 動作確認

これで記述が完了したので、docker compose upを実行して動作確認してみます。
動作確認には、Postmanを使用しています。

POST

まずは、テーブルに何も入っていないのでhttp://localhost:8080/usersにPOSTメソッドを送信してレコードを増やしてみます。
postman post user.png
レコードの追加が成功し、レスポンスとして追加したレコードの情報が返ってきました(^▽^)/

GET

次に、GETメソッドを使用してUserテーブルのレコードを取得したいと思います。
postman get user.png
こちらも正常に動作が完了し、レコードを取得できていますね。👀
(筆者が何度もPOSTしたせいで、先ほど追加したレコードが6番目になっているのはお気になさらず)

PUT

先ほど追加したレコードの情報を更新してみましょう。
postman put user.png
更新が成功し、レスポンスとして更新したレコードの情報が返ってくることを確認できました。

DELETE

最後に、更新したレコードを削除してみましょう。
postman delete user.png
こちらも、成功時に削除したレコードの情報が返ってくることを確認できました。

これで、GoからMySQLのDBにCRUD操作を行うAPIサーバを構築することができました!

さいごに

最後までご覧いただきありがとうございます。
今回は、CRUD操作を行うAPIサーバをGoで構築する所まで行いました。
気が向いたら(需要があるかは知りませんが)今回のAPIサーバとNext.jsを連携して、フロントエンドからAPIを叩いてCRUD操作を行うまでを記事に書きたいと思います。

8
0
4

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
8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?