ginを使ってMVCモデルのWebアプリを作成する

2022/03/23に公開

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

golangのフレームワークginを使って簡単なMVCモデルWebサイトを作成してみます。

MVCモデル

Model, View, Controllerと役割を分けて整理するモデルです。

Model

  • ビジネスロジックを担当する。具体的にはデータベースへアクセスしCRUDを実行する。
  • データをデータベースやgolangで使いやすい形に変換する。

View

  • ブラウザに表示するhtml等、ユーザーが見て確認できる部分を担当する。
  • ここではgolangの標準パッケージhtml/templateをViewとする。

Controller

  • ModelとViewの橋渡し役を担当する。
  • クライアントから送られたURIをrouter.goでルーティングしModelでCRUDを実行→Modelで得たデータをViewで表示など。
  • ビジネスロジックは全てModelに任せる。(Controllerは薄く作る。)

ディレクトリ構成

mvc_test
├── cmd
│   └── mvc_test
│       └── main.go
├── controller
│   ├── blog_controller.go
│   └── router.go
├── go.mod
├── go.sum
├── model
│   ├── blog_model.go
│   └── database.go
└── view
    ├── create.html
    ├── delete.html
    ├── edit.html
    ├── index.html
    └── show.html

MySQLをDocker経由でインストールしましたが、記事内では省略しました。
Dockerやデータベースについてはこちらの記事をご参照ください。
https://zenn.dev/ajapa/articles/443c396a2c5dd1
https://zenn.dev/ajapa/articles/03dcf8fd12d086

ルーティング実装

まずはサーバーへURIが投げられた際のルーティングです。

router.go
package controller

import (
	"github.com/gin-gonic/gin"
)

func GetRouter() *gin.Engine {
	r := gin.Default()
	r.LoadHTMLGlob("view/*html")
	r.GET("/", ShowAllBlog)
	r.GET("/show/:id", ShowOneBlog)
	r.GET("/create", ShowCreateBlog)
	r.POST("/create", CreateBlog)
	r.GET("/edit/:id", ShowEditBlog)
	r.POST("/edit", EditBlog)
	r.GET("/delete/:id", ShowCheckDeleteBlog)
	r.POST("/delete", DeleteBlog)
	return r
}

router.goでginを使ったルーティングを実装しています。
Handlerとして登録している関数は同じcontrollerパッケージのblog_controller.goのものです。

main.go
package main

import (
	"mvc_test/controller"
)

func main() {
	router := controller.GetRouter()
	router.Run(":8080")
}

controllerパッケージのrouter.goのGetRouter()を実行しgin.Engineを取得します。

Model実装

https://zenn.dev/ajapa/articles/aa9b59dd30c501
database.goはこちらの記事のものを使います。
packageだけblog_model.goに合わせてmodelとします。

blog_model.go
package model

import (
	"gorm.io/gorm"
)

type BlogEntity struct {
	gorm.Model
	Title string
	Body  string
}

func GetAll() (datas []BlogEntity) {
	result := Db.Find(&datas)
	if result.Error != nil {
		panic(result.Error)
	}
	return
}

func GetOne(id int) (data BlogEntity) {
	result := Db.First(&data, id)
	if result.Error != nil {
		panic(result.Error)
	}
	return
}

func (b *BlogEntity) Create() {
	result := Db.Create(b)
	if result.Error != nil {
		panic(result.Error)
	}
}

func (b *BlogEntity) Update() {
	result := Db.Save(b)
	if result.Error != nil {
		panic(result.Error)
	}
}

func (b *BlogEntity) Delete() {
	result := Db.Delete(b)
	if result.Error != nil {
		panic(result.Error)
	}
}

GetAll(全件取得)、GetOne(1件取得)でデータベースに登録したBlogEntityデータを取得します。
またCreate(新規作成)、Update(更新)、Delete(削除)はBlogEntityのメソッドとします。(funcと関数名の間に(b *BlogEntity)と記述すると、[データ].メソッド()という形で使用できる。)

Controller実装

blog_controller.go
package controller

import (
	"fmt"
	"mvc_test/model"
	"strconv"

	"github.com/gin-gonic/gin"
)

func ShowAllBlog(c *gin.Context) {
	datas := model.GetAll()
	c.HTML(200, "index.html", gin.H{"datas": datas})
}

func ShowOneBlog(c *gin.Context) {
	id, _ := strconv.Atoi(c.Param("id"))
	data := model.GetOne(id)
	c.HTML(200, "show.html", gin.H{"data": data})
}

func ShowCreateBlog(c *gin.Context) {
	c.HTML(200, "create.html", gin.H{})
}

func CreateBlog(c *gin.Context) {
	title := c.PostForm("title")
	body := c.PostForm("body")
	data := model.BlogEntity{Title: title, Body: body}
	data.Create()
	c.Redirect(301, "/")
}

func ShowEditBlog(c *gin.Context) {
	id, _ := strconv.Atoi(c.Param("id"))
	data := model.GetOne(id)
	c.HTML(200, "edit.html", gin.H{"data": data})
}

func EditBlog(c *gin.Context) {
	id, _ := strconv.Atoi(c.PostForm("id"))
	data := model.GetOne(id)
	title := c.PostForm("title")
	data.Title = title
	body := c.PostForm("body")
	data.Body = body
	data.Update()
	c.Redirect(301, "/")
}

func ShowCheckDeleteBlog(c *gin.Context) {
	id, _ := strconv.Atoi(c.Param("id"))
	data := model.GetOne(id)
	c.HTML(200, "delete.html", gin.H{"data": data})
}

func DeleteBlog(c *gin.Context) {
	id, _ := strconv.Atoi(c.PostForm("id"))
	fmt.Println("delete:", id)
	data := model.GetOne(id)
	data.Delete()
	c.Redirect(301, "/")
}

router.goから呼び出す関数を記述しました。
データの取得・保存・成形は全てModelに任せController内はなるべく薄くするべきとのことですが、初めて使うのであまりうまくできないないように思います...。

View実装

index.html
<!DOCTYPE html>
<html>
    <head>
        <meta httpequiv="ContentType" content="text/html;charset=utf8">
        <title>INDEX</title>
    </head>
    <body>
        <h1>INDEX</h1>
        {{ range .datas }}
        <p>{{ .ID }}: <a href="/show/{{.ID}}">{{.Title}}</a> / <a href="/edit/{{.ID}}">編集</a> / <a href="/delete/{{.ID}}">削除</a></p>
        {{ end }}
        <p><a href="/create">新規作成</a></p>
    </body>
</html>

BlogEntityの一覧を表示します。

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

BlogEntity新規作成の情報を入力しPOST(/create)でサーバーへ送信します。

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

BlogEntity情報を表示します。

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

BlogEntity情報を表示・変更しPOST(/edit)でサーバーへ送信します。

delete.html
<!DOCTYPE html>
<html>
    <head>
        <meta httpequiv="ContentType" content="text/html;charset=utf8">
        <title>記事削除(ID:{{.data.ID}})</title>
    </head>
    <body>
        <form method="POST" action="/delete" id="delete_form">
        <input type="hidden" name="id" value="{{ .data.ID }}" />
        <p>タイトル: {{.data.Title}}</p>
        <p>本文:</p>
        <p>{{.data.Body}}</p>
        <h2>削除しますか?</h2>
    </form>
    <p>
        <button type="submit" form="delete_form">削除</button>
        <a href="/"><input type="button" value="キャンセル" /></a>
    </p>
    </body>
</html>

削除予定のBlogEntity情報を表示しPOST(/delete)でサーバーへ送信します。

参考

https://dev.classmethod.jp/articles/go-sample-rest-api/
https://bsblog.casareal.co.jp/archives/4822
https://bsblog.casareal.co.jp/archives/5121
https://zenn.dev/7oh/articles/6338a8ccd470c7

Discussion