golangとMySQLでCRUDを実装する
【環境】
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を使って構築する方法はこちらでまとめました。
またgolangでホットリロードを行うAirも導入しています。
詳しくはこちらをご参照ください。
ディレクトリ構成
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関係
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
FROM mysql:8.0
ENV LANG ja_JP.UTF-8
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のみ変更しました。
cmd = "go build -o ./tmp/main ./cmd/go_blog/"
詳細はこちらをご参照ください。
実装部分
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
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
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
<!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
<!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
<!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
<!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
<!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を利用し改行を機能させています。
参考
Discussion