😀

golangとMySQLでCRUDを実装する

2022/03/15に公開

【環境】
MacBook Air (M1, 2020)
OS: MacOS Big Sur version11.6
Docker Desktop for Mac version4.5.0

golangとMySQLでCRUD(Create/Read/Update/Delete)を実装した時の記録です。
golangとMySQLをdocker-composeを使って構築する方法はこちらでまとめました。
https://zenn.dev/ajapa/articles/443c396a2c5dd1
またgolangでホットリロードを行うAirも導入しています。
詳しくはこちらをご参照ください。
https://zenn.dev/ajapa/articles/bc399c7e4c0def

ディレクトリ構成

go_blog
├── build
│   ├── app
│   │   └── Dockerfile
│   └── db
│       ├── Dockerfile
│       └── init
│           └── create_table.sh
├── cmd
│   └── go_blog
│       └── main.go
├── docker-compose.yml
├── go.mod
├── go.sum
├── internal
│   ├── article
│   │   └── article.go
│   └── utility
│       └── database.go
└── web
    └── template
        ├── create.html
        ├── delete.html
        ├── edit.html
        ├── index.html
        └── show.html

buildのappディレクトリがgolang、dbディレクトリがMySQLの設定です。
main.goはcmd/[プロジェクト名ディレクトリ]/main.goに配置します。
外部から使われる予定のないプログラムは全てinternalディレクトリに入れることにしました。

Docker関係

docker-compose.yml
version: '3.8'

services:
  go_blog:
    container_name: go_blog
    build:
      context: ./build/app
      dockerfile: Dockerfile
    tty: true
    ports:
      - 8080:8080
    env_file:
      - ./build/db/.env
    depends_on:
      - db
    volumes:
      - type: bind
        source: .
        target: /go/app
    networks:
      - golang_test_network

  db:
    container_name: db
    build:
      context: ./build/db
      dockerfile: Dockerfile
    tty: true
    platform: linux/amd64
    ports:
      - 3306:3306
    env_file:
      - ./build/db/.env
    volumes:
      - type: volume
        source: mysql_test_volume
        target: /var/lib/mysql
      - type: bind
        source: ./build/db/init
        target: /docker-entrypoint-initdb.d
    networks:
      - golang_test_network

volumes:
  mysql_test_volume:
    name: mysql_test_volume

networks:
  golang_test_network:
    external: true
build > db > Dockerfile
FROM mysql:8.0
ENV LANG ja_JP.UTF-8
build > app > Dockerfile
FROM golang:1.17.7-alpine
RUN apk update && apk add git
RUN go get github.com/cosmtrek/air@v1.29.0
RUN mkdir -p /go/app
WORKDIR /go/app

CMD ["air", "-c", ".air.toml"]

alpineサーバーのgolangを使います。ホットリロードのAirも導入もしました。
.air.tomlはcmdのみ変更しました。

.air.toml
cmd = "go build -o ./tmp/main ./cmd/go_blog/"

詳細はこちらをご参照ください。

実装部分

main.go

main.go
package main

import (
	"go_blog/internal/article"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", article.Index)
	http.HandleFunc("/show", article.Show)
	http.HandleFunc("/create", article.Create)
	http.HandleFunc("/edit", article.Edit)
	http.HandleFunc("/delete", article.Delete)

	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal("ListenAndServe:", err)
	}
}

main.goはなるべくシンプルにしたほうがいいということで、httpのハンドリングのみにしました。
articleパッケージの各CRUDメソッドにハンドリングしています。

database.go

database.go
package utility

import (
	"database/sql"
	"fmt"
	"log"
	"os"
	"time"

	_ "github.com/go-sql-driver/mysql"
)

var Db *sql.DB

func init() {
	user := os.Getenv("MYSQL_USER")
	pw := os.Getenv("MYSQL_PASSWORD")
	db_name := os.Getenv("MYSQL_DATABASE")
	var path string = fmt.Sprintf("%s:%s@tcp(db:3306)/%s?charset=utf8&parseTime=true", user, pw, db_name)
	var err error
	if Db, err = sql.Open("mysql", path); err != nil {
		log.Fatal("Db open error:", err.Error())
	}
	checkConnect(100)

	fmt.Println("db connected!!")
}

func checkConnect(count uint) {
	var err error
	if err = Db.Ping(); err != nil {
		time.Sleep(time.Second * 2)
		count--
		fmt.Printf("retry... count:%v\n", count)
		checkConnect(count)
	}
}

init関数でMySQL接続処理を行います。
init関数は自動的に呼び出され、main関数より早く実行されます。
MySQLの準備が完了し接続されるまでcheckConnectが呼ばれ、2秒ごとに接続を試みます。
以後MySQLとやりとりがしたい時はdatabase.goのDb変数を参照します。
その他詳細はこちらをご参照ください。

article.go

article.go
package article

import (
	"fmt"
	"go_blog/internal/utility"
	"html/template"
	"log"
	"net/http"
	"strings"
)

type Article struct {
	Id    int
	Title string
	Body  string
}

var tmpl *template.Template

func init() {
	funcMap := template.FuncMap{
		"nl2br": func(text string) template.HTML {
			return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "<br />", -1))
		},
	}

	tmpl, _ = template.New("article").Funcs(funcMap).ParseGlob("web/template/*")
}

func Index(w http.ResponseWriter, r *http.Request) {
	selected, err := utility.Db.Query("SELECT * FROM Article")
	if err != nil {
		panic(err.Error())
	}
	data := []Article{}
	for selected.Next() {
		article := Article{}
		err = selected.Scan(&article.Id, &article.Title, &article.Body)
		if err != nil {
			panic(err.Error())
		}
		data = append(data, article)
	}
	selected.Close()
	if err := tmpl.ExecuteTemplate(w, "index.html", data); err != nil {
		log.Fatal(err)
	}
}

func Show(w http.ResponseWriter, r *http.Request) {
	id := r.URL.Query().Get("id")
	selected, err := utility.Db.Query("SELECT * FROM Article WHERE id=?", id)
	if err != nil {
		panic(err.Error())
	}
	article := Article{}
	for selected.Next() {
		err = selected.Scan(&article.Id, &article.Title, &article.Body)
		if err != nil {
			panic(err.Error())
		}
	}
	selected.Close()
	tmpl.ExecuteTemplate(w, "show.html", article)
}

func Create(w http.ResponseWriter, r *http.Request) {
	if r.Method == "GET" {
		tmpl.ExecuteTemplate(w, "create.html", nil)
	} else if r.Method == "POST" {
		title := r.FormValue("title")
		body := r.FormValue("body")
		insert, err := utility.Db.Prepare("INSERT INTO Article(title, body) VALUES(?,?)")
		if err != nil {
			panic(err.Error())
		}
		insert.Exec(title, body)
		http.Redirect(w, r, "/", 301)
	}
}

func Edit(w http.ResponseWriter, r *http.Request) {
	if r.Method == "GET" {
		id := r.URL.Query().Get("id")
		selected, err := utility.Db.Query("SELECT * FROM Article WHERE id=?", id)
		if err != nil {
			panic(err.Error())
		}
		article := Article{}
		for selected.Next() {
			err = selected.Scan(&article.Id, &article.Title, &article.Body)
			if err != nil {
				panic(err.Error())
			}
		}
		selected.Close()
		tmpl.ExecuteTemplate(w, "edit.html", article)
	} else if r.Method == "POST" {
		title := r.FormValue("title")
		body := r.FormValue("body")
		id := r.FormValue("id")
		insert, err := utility.Db.Prepare("UPDATE Article SET title=?, body=? WHERE id=?")
		if err != nil {
			panic(err.Error())
		}
		insert.Exec(title, body, id)
		http.Redirect(w, r, "/", 301)
}

func Delete(w http.ResponseWriter, r *http.Request) {
	if r.Method == "GET" {
		id := r.URL.Query().Get("id")
		selected, err := utility.Db.Query("SELECT * FROM Article WHERE id=?", id)
		if err != nil {
			panic(err.Error())
		}
		article := Article{}
		for selected.Next() {
			err = selected.Scan(&article.Id, &article.Title, &article.Body)
			if err != nil {
				panic(err.Error())
			}
		}
		selected.Close()
		tmpl.ExecuteTemplate(w, "delete.html", article)
	} else if r.Method == "POST" {
		id := r.FormValue("id")
		insert, err := utility.Db.Prepare("DELETE FROM Article WHERE id=?")
		if err != nil {
			panic(err.Error())
		}
		insert.Exec(id)
		http.Redirect(w, r, "/", 301)
	}
}

  • 記事のCRUD(Create(新規作成)Read(読み込み)Update(更新)Delete(削除))と記事の一覧表示(Index)を定義しました。
  • http.RequestのMethodを識別することで、1つの関数内でGETとPOST両方の役割を記述できます。
  • utility.DbでMySQLにアクセスすることができます。
  • デフォルトでは改行が機能しなかったので、funcMapに専用の関数を登録しました。(参考)
    template.FuncMapを使うとtemplate(html)上で自分の定義した関数を使うことができます。

index.html

index.html
<!DOCTYPE html>
<html>
    <head>
        <meta httpequiv="ContentType"content="text/html;charset=utf8">
        <title>記事一覧</title>
    </head>
    <body>
        <p><a href="/create">新規作成</a></p>
        <table border="1">
            <thead>
            <tr>
              <td>Id</td>
              <td>Title</td>
              <td>Show</td>
              <td>Edit</td>
              <td>Del</td>
            </tr>
             </thead>
             <tbody>
        {{ range . }}
            <tr>
                <td>{{ .Id }}</td>
                <td> {{ .Title }} </td>
                <td> <a href="/show?id={{ .Id }}">表示</a> </td>
                <td> <a href="/edit?id={{ .Id }}">編集</a> </td>
                <td> <a href="/delete?id={{ .Id }}">削除</a> </td>
            </tr>
    {{ end }}
        </tbody>
        </table>
    </body>
</html>

golangから渡したデータを使いたい時は{{ .[変数名] }}で参照することができます。
{{range .}}は渡された配列データをfor文のように取り出します。

show.html

show.html
<!DOCTYPE html>
<html>
    <head>
        <meta httpequiv="ContentType"content="text/html;charset=utf8">
        <title>記事表示(ID:.Id)</title>
    </head>
    <body>
        <p>タイトル: {{.Title}}</p>
        <p>本文:</p>
        <p>{{nl2br .Body}}</p>
        
        <p><a href="/edit?id={{.Id}}"><input type="button" value="編集" /></a>
        <a href="/index"><input type="button" value="戻る" /></a></p>
    </body>
</html>

記事Id, タイトル, 本文を表示します。
{{[funcMapで定義した関数] .[変数名]}}で定義した関数を使うことができます。
nl2br関数に渡されたBody変数内の\nは全て
に変換され改行が機能します。

create.html

create.html
<!DOCTYPE html>
<html>
    <head>
        <meta httpequiv="ContentType"content="text/html;charset=utf8">
        <title>記事新規作成</title>
    </head>
    <body>
        <form method="POST" action="create">
            <p>タイトル: <input type="text" name="title" value="" /></p>
            <p>本文:</p>
            <p><textarea name="body" rows="10" cols="40"></textarea></p>
            <p><input type="submit" value="作成" /> 
                <a href="/index"><input type="button" value="戻る" /></a></p>
        </form>
    </body>
</html>

タイトルと本文を入力しPOSTで送信します。
actionにcreateと定義することでmain関数のhttp.HandleFunc("/create", article.Create)が実行され、article.goのCreate関数が実行されます。

edit.html

edit.html
<!DOCTYPE html>
<html>
    <head>
        <meta httpequiv="ContentType"content="text/html;charset=utf8">
        <title>記事編集(ID:{{.Id}})</title>
    </head>
    <body>
        <form method="POST" action="edit">
            <input type="hidden" name="id" value="{{ .Id }}" />
            <p>タイトル: <input type="text" name="title" value="{{.Title}}" /></p>
            <p>本文:</p>
            <p><textarea name="body" rows="10" cols="40">{{.Body}}</textarea></p>
            <p><input type="submit" value="更新" /> 
                <a href="/index"><input type="button" value="戻る" /></a></p>
        </form>
    </body>
</html>

createとほぼ同じです。
hiddenに保存したIdを送ることで、Edit関数内で新規作成ではなく上書き更新されます。

delete.html

delete.html
<!DOCTYPE html>
<html>
    <head>
        <meta httpequiv="ContentType"content="text/html;charset=utf8">
        <title>記事削除(ID:{{.Id}})</title>
    </head>
    <body>
        <form method="POST" action="delete">
            <input type="hidden" name="id" value="{{ .Id }}" />
            <p>タイトル: {{.Title}}</p>
            <p>本文:</p>
            <p>{{nl2br .Body}}</p>

            <p><input type="submit" value="削除" /> 
                <a href="/index"><input type="button" value="キャンセル" /></a></p>
        </form>
    </body>
</html>

hiddenに保存したIdをarticle.goのDelete関数に送り、記事を削除します。
ここでもnl2brを利用し改行を機能させています。

参考

https://dev.classmethod.jp/articles/go-sample-rest-api/
https://ryotarch.com/go-lang/example-of-golang-crud-using-mysql/
https://www.golangprograms.com/example-of-golang-crud-using-mysql-from-scratch.html
https://qiita.com/maroKanatani/items/0ed49295f9e3f65d61f6
https://gist.github.com/tomsato/64c22ebf60927fb17a5ec2bb5e595c13
https://maku77.github.io/hugo/go/template.html
https://qiita.com/hnw/items/7bcfce84a99b0fc66a5b

Discussion