taisablog

taisa's engineer blog

Go

Go言語 GORM+GinでTODOリストを作ってみた

投稿日:


前回の「Go言語 ORMライブラリ GORMの使い方」に続いて「GORM+Gin」でTODOリストを作ってみました。使い方は「GitHubのREADME」を参考にしました。できたものは下記URLから確認できます。装飾は別途やれればと。
http://gin.taisablog.com/todo

事前情報

ルーティングは通常のフォームだとPUT/DELETEが使えないので以下のようにしました。

[GIN-debug] GET    /todo                     --> main.main.func1 (3 handlers) // 一覧表示
[GIN-debug] POST   /todo                     --> main.main.func2 (3 handlers) // 新規作成
[GIN-debug] GET    /todo/:id                 --> main.main.func3 (3 handlers) // 編集画面表示
[GIN-debug] POST   /todo/edit/:id            --> main.main.func4 (3 handlers) // 編集
[GIN-debug] POST   /todo/delete/:id          --> main.main.func5 (3 handlers) // 削除

main.goだけで作成した場合

main.goに全ての処理を記述しています。

package main
import (
  "github.com/gin-gonic/gin"
  "github.com/jinzhu/gorm"
  _ "github.com/jinzhu/gorm/dialects/mysql"
  "net/http"
  "strconv"
)
type Todo struct {
  gorm.Model
  Text   string
  Status uint64
}
func main() {
  db, err := gorm.Open("mysql", "gorm:gorm@/sandbox_gin?charset=utf8mb4&parseTime=True&loc=Local")
  if err != nil {
    panic("データベースへの接続に失敗しました")
  }
  defer db.Close()
  db.LogMode(true)
  db.AutoMigrate(&Todo{})
  r := gin.Default()
  r.LoadHTMLGlob("templates/*")
  // 一覧画面
  r.GET("/todo", func(c *gin.Context) {
    var todos []Todo
    db.Find(&todos)
    c.HTML(http.StatusOK, "index.html", gin.H{
      "todos": todos,
    })
  })
  // 新規作成
  r.POST("/todo", func(c *gin.Context) {
    text, _ := c.GetPostForm("text")
    status, _ := c.GetPostForm("status")
    istatus, _ := strconv.ParseUint(status, 10, 32)
    db.Create(&Todo{Text: text, Status: istatus})
    c.Redirect(http.StatusMovedPermanently, "/todo")
  })
  // 編集画面
  r.GET("/todo/:id", func(c *gin.Context) {
    todo := Todo{}
    id := c.Param("id")
    db.First(&todo, id)
    c.HTML(http.StatusOK, "edit.html", gin.H{
      "todo": todo,
    })
  })
  // 編集
  r.POST("/todo/edit/:id", func(c *gin.Context) {
    todo := Todo{}
    id := c.Param("id")
    text, _ := c.GetPostForm("text")
    status, _ := c.GetPostForm("status")
    istatus, _ := strconv.ParseUint(status, 10, 32)
    db.First(&todo, id)
    todo.Text = text
    todo.Status = istatus
    db.Save(&todo)
    c.Redirect(http.StatusMovedPermanently, "/todo")
  })
  // 削除
  r.POST("/todo/delete/:id", func(c *gin.Context) {
    todo := Todo{}
    id := c.Param("id")
    db.First(&todo, id)
    db.Delete(&todo)
    c.Redirect(http.StatusMovedPermanently, "/todo")
  })
  r.Run(":9000")
}

ファイルを分割した場合

main.goだけだとちょっと味気ないのでWebフレームワークっぽい構成にしてみました。書き方は色々だと思うので「参考」としてみてもらえればと思います。

ディレクトリ構成

.
├── controllers
│   └── todo.go
├── db
│   └── db.go
├── main.go
├── models
│   └── todo.go
├── router
│   └── router.go
└── templates
    ├── edit.html
    └── index.html

main.go

DB初期化とRouter初期化の呼び出し

package main
import (
  "github.com/taisa831/sandbox-gin/db"
  "github.com/taisa831/sandbox-gin/router"
)
func main() {
  dbConn := db.Init()
  router.Router(dbConn)
}

db/db.go

DB初期化

package db
import (
  "github.com/jinzhu/gorm"
  _ "github.com/jinzhu/gorm/dialects/mysql"
  "github.com/taisa831/sandbox-gin/models"
)
func Init() *gorm.DB {
  db, err := gorm.Open("mysql", "gorm:gorm@/sandbox_gin?charset=utf8mb4&parseTime=True&loc=Local")
  if err != nil {
    panic("データベースへの接続に失敗しました")
  }
  db.LogMode(true)
  db.AutoMigrate(&models.Todo{})
  return db
}

models/todo.go

modelsにはモデルの情報だけ記述

package models
import "github.com/jinzhu/gorm"
type Todo struct {
 gorm.Model
 Text string
 Status uint64
}

router/router.go

ルート情報を記述して起動

package router
import (
  "github.com/gin-gonic/gin"
  "github.com/jinzhu/gorm"
  "github.com/taisa831/sandbox-gin/controllers"
)
func Router(dbConn *gorm.DB) {
  todoHandler := controllers.TodoHandler{
    Db: dbConn,
  }
  r := gin.Default()
  r.LoadHTMLGlob("templates/*")
  r.GET("/todo", todoHandler.GetAll) // 一覧画面
  r.POST("/todo", todoHandler.CreateTask) // 新規作成
  r.GET("/todo/:id", todoHandler.EditTask) // 編集画面
  r.POST("/todo/edit/:id", todoHandler.UpdateTask) // 更新
  r.POST("/todo/delete/:id", todoHandler.DeleteTask) // 削除
  r.Run(":9000")
}

controllers/todo.go

実際にWebから呼び出された時の処理

package controllers
import (
  "github.com/gin-gonic/gin"
  "github.com/jinzhu/gorm"
  "github.com/taisa831/sandbox-gin/models"
  "net/http"
  "strconv"
)
type TodoHandler struct {
  Db *gorm.DB
}
func (h *TodoHandler) GetAll(c *gin.Context) {
  var todos []models.Todo
  h.Db.Find(&todos)
  c.HTML(http.StatusOK, "index.html", gin.H{
    "todos": todos,
  })
}
func (h *TodoHandler) CreateTask(c *gin.Context) {
  text, _ := c.GetPostForm("text")
  status, _ := c.GetPostForm("status")
  istatus, _ := strconv.ParseUint(status, 10, 32)
  h.Db.Create(&models.Todo{Text: text, Status: istatus})
  c.Redirect(http.StatusMovedPermanently, "/todo")
}
func (h *TodoHandler) EditTask(c *gin.Context) {
  todo := models.Todo{}
  id := c.Param("id")
  h.Db.First(&todo, id)
  c.HTML(http.StatusOK, "edit.html", gin.H{
    "todo": todo,
  })
}
func (h *TodoHandler) UpdateTask(c *gin.Context) {
  todo := models.Todo{}
  id := c.Param("id")
  text, _ := c.GetPostForm("text")
  status, _ := c.GetPostForm("status")
  istatus, _ := strconv.ParseUint(status, 10, 32)
  h.Db.First(&todo, id)
  todo.Text = text
  todo.Status = istatus
  h.Db.Save(&todo)
  c.Redirect(http.StatusMovedPermanently, "/todo")
}
func (h *TodoHandler) DeleteTask(c *gin.Context) {
  todo := models.Todo{}
  id := c.Param("id")
  h.Db.First(&todo, id)
  h.Db.Delete(&todo)
  c.Redirect(http.StatusMovedPermanently, "/todo")
}

templates/index.html




    
    TODOリスト
    



    新規追加:
    
        未対応
        対応中
        完了
    
    


{{range .todos}} {{end}}
TODO ステータス
{{.Text}} {{if eq .Status 1}} 未対応 {{else if eq .Status 2}} 対応中 {{else if eq .Status 3}} 完了 {{end}} 編集 削除
$(function(){ $('.delete').on('click', function(){ $('#frmDelete').attr('action', '/todo/delete/' + $(this).data('id')) $('#frmDelete').submit() }) })

templates/edit.html




    
    TODOリスト 編集



    テキスト
未対応 対応中 完了

GitHubはこちら
https://github.com/taisa831/sandbox-gin

-Go
-

執筆者:

関連記事

Golang 1.13 released! The difference from 1.12 to 1.13

Golang 1.13 was released in 3 September 2019. This post has difference from 1.12 to 1.13. I check the changes at the release notes and difference of src. Release Notes Go 1.13 Release Notes The latest Go release, version 1.13, arrives six months after Go 1.12. Most of its changes are in the implementation of the toolchain, runtime, and libraries. As always, the release maintains the Go 1 promise of compatibility. We expect almost all Go programs to continue to compile and run as before. Diff on GitHub Release branch.go1.13 by taisa831 · Pull Request #1 · taisa831/go This PR …

Golangを使ってJWTを15分で理解する

JWTとは JWT(ジョットと言うらしい)はJSON Web Tokenの略で、JSONをベースとしたアクセストークンのためのオープン標準 (RFC 7519) です。色々記事を見ましたが、最終的にWikipediaが分かりやすく一番参考にしました。https://ja.wikipedia.org/wiki/JSON_Web_Token JWTの構造 JWTは以下の3つの要素をピリオドで区切った文字列で構成されます。 ヘッダー 署名生成に使用するアルゴリズムを格納します。下記のHS256は、このトークンがHMAC-SHA256で署名されていることを示しています。署名アルゴリズムとしては、SHA-256を使用したHMAC (HS256) や、SHA-256を使用したRSA署名 (RS256) がよく用いられます。 { “alg” : “HS256”, “typ” : “JWT” } ペイロード 認証情報などのクレームを格納します。クレームとはペイロードに含める以下のような標準フィールド(クレーム)を指します。JWTの仕様では、トークンに一般的に含まれる7つの標準フィールドが定義されています。また用途に応じた独自のカスタムフィールドを含むこともできます。下記の例では、トークン発行日時を示す標準のクレーム (iat) と、カスタムクレーム (loggedInAs) を格納しています。 { “loggedInAs” : “admin”, “iat” : 1422779638 } 7つのペイロードの標準クレーム 署名 トークン検証用の署名です。署名はヘッダーとペイロードをBase64urlエンコーディングしてピリオドで結合したものから生成します。署名はヘッダーで指定された暗号化アルゴリズムにより生成されます。下記はHMAC-SHA256形式でのコード例です。 HMAC-SHA256(base64urlEncoding(header) + ‘.’ + base64urlEncoding(payload), ‘secret key’) JWTを使用するにあたって JWTはトークンが返され、それをローカルに保存して利用します(主にlocal storageやsession storageが用いられますが、セッションIDのようにCookieを用いる場合もあります。) 認証時にはAuthorizationヘッダーでBearerスキーマを利用します。またサーバー上に認証状態を保持しないステートレスな認証方式です。その為JWT単体ではトークンを無効にすることが出来ません。サーバーに状態を保持すれば可能ですが、その場合ステートレスの利点は失われることになります。さて、ここまではほぼ Wikipedia に書いてある内容そのままです。ここから実際にGo/GinのJWT Middlewareを使って実際の動作を確認してみます。 Go/GinのJWT Middlewareを使った動作確認 利用するJWT Middlewareについて ここでは、「https://github.com/gin-gonic/gin」 を使う前提で、次のMiddlewareを利用します。「https://github.com/appleboy/gin-jwt」。このMiddlewareは、auth_jwt.goの1ファイルでで構成されていて、「https://github.com/dgrijalva/jwt-go」 をGin用に薄くラップしたものです。jwt-goはトークンを作成したりパースしたり様々な機能が用意されています。 サンプルソース サンプルソースは、https://github.com/appleboy/gin-jwt/blob/master/README.md に載っているのでこれを元に確認します。処理は大きく「ログイン時にToken発行する」と「トークン認証&処理実行する」の2種類あります。 ログイン時にToken発行する ログイン時にTokenを発行する処理は、LoginHandlerです。Routerでは次のように定義しています。LoginHandlerではAuthenticatorとPayloadFuncが呼ばれる為、Middlewareにてこれらを実装する必要があります。 r.POST(“/login”, authMiddleware.LoginHandler) Authenticatorはログイン認証の為の関数です。例では固定値が設定されていますが、実際は主にDBから値を取得することになると思います。PayloadFuncはペイロードに含めるクレームを設定します。ペイロードには任意のクレームを追加可能なので、ログインIDとなるuserIDをセットしています。 // ログインに基づいたユーザの認証振る舞いをするコールバック Authenticator: func(c *gin.Context) (interface{}, error) { var loginVals login if err := c.ShouldBind(&loginVals); err != nil { return “”, jwt.ErrMissingLoginValues } userID := loginVals.Username password := loginVals.Password // …

[Golang]Goのio/ioutilパッケージは分かりやすくて使いやすい

Goのioパッケージは主にインターフェースになっていて他のパッケージで多く実装されています。またioパッケージにもパブリックな関数がありファイルの入出力はできますが少し細かい処理になります。io/ioutilパッケージを使うとファイルの入出力処理が簡単にできます。以下にio/ioutilパッケージを使った処理とそれに対するテストコードを記載します。 io/ioutil/ioutil.go ReadAll() func ReadAll() string { file, _ := os.Open(“testdata/src.txt”) b, _ := ioutil.ReadAll(file) return string(b) } ReadAllテスト func TestReadAll(t *testing.T) { str := ReadAll() if str != “0123456789” { t.Errorf(“TestReadAll Error. %s”, str) } } ReadFile() func ReadFile() string { b, _ := ioutil.ReadFile(“testdata/src.txt”) return string(b) } ReadFileテスト func TestReadFile(t *testing.T) { str := ReadFile() if str != “0123456789” { t.Errorf(“TestReadAll Error. %s”, str) } } WriteFile() func WriteFile() string { b := []byte(“0123456789”) _ = ioutil.WriteFile(“testdata/dst.txt”, b, os.ModePerm) b, _ = ioutil.ReadFile(“testdata/dst.txt”) return string(b) } WriteFileテスト func TestWriteFile(t *testing.T) { str := WriteFile() if str != “0123456789” …

no image

docker-compose&dockerizeでGo+MySQLのWebサーバーを起動する

docker-composeでGo + MySQLを起動する場合、MySQLの起動を待ってGoのWebサーバーを起動する必要があります。実現するにはどうやらスクリプトを書かないとだめらしいですが、dockerizeを使うと簡単に実現できます。 サンプル用Goサーバを作成する 起動時にusersテーブルを作成し、リクエスト時にusersテーブルにレコードをinsertする簡単なサンプルアプリケーションです。 package main import ( “database/sql” “encoding/json” “fmt” “log” “net/http” “os” _ “github.com/go-sql-driver/mysql” “github.com/joho/godotenv” ) type User struct { FirstName string `json:”firstName”` LastName string `json:”lastName”` } var db *sql.DB func main() { err := godotenv.Load() if err != nil { log.Fatal(err) } db = Conn() defer db.Close() _, err = db.Exec(“CREATE TABLE IF NOT EXISTS users(id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, firstname VARCHAR(255) NOT NULL, lastname VARCHAR(255) NOT NULL)”) if err != nil { log.Fatal(err) } http.HandleFunc(“/users”, users) err = http.ListenAndServe(os.Getenv(“LISTEN_PORT”), nil) if err != nil { log.Fatal(err) } } func Conn() *sql.DB { db, err …

GORMでよく使うSQLの書き方

GORMでよく使うSQLの書き方をメモとして残しておきます。詳細は記事最後の参考リンクの公式ドキュメントで確認できます。 CREATE insertするだけであれば create を利用します。 db.Create(&models.User{Name: “user”}) # 実行SQL INSERT INTO “users” (“created_at”,”updated_at”,”name”,”email”) VALUES (‘2020-04-25 11:22:00′,’2020-04-25 11:22:00′,’user’,0) UPDATE 特定のフィールドだけ更新したい場合、 Update と Updates を使います。 update 一つのフィールドだけアップデートします。 user := &models.User{Name: “user”} db.Create(user) db.Model(user).Update(“name”, “user2”) # 実行SQL INSERT INTO “users” (“created_at”,”updated_at”,”name”,”email”) VALUES (‘2020-04-25 11:34:39′,’2020-04-25 11:34:39′,’user’,”) UPDATE “users” SET “name” = ‘user2’, “updated_at” = ‘2020-04-25 11:34:39’ WHERE “users”.”id” = 5 updates updates は map を利用して複数のフィールドをアップデートします。 user := &models.User{Name: “user”} db.Create(user) db.Model(user).Updates(map[string]interface{}{“name”: “user3”, “email”: “g5.taisa831@gmail.com”}) # 実行SQL INSERT INTO “users” (“created_at”,”updated_at”,”name”,”email”) VALUES (‘2020-04-25 11:39:52′,’2020-04-25 11:39:52′,’user’,”) UPDATE “users” SET “email” = ‘g5.taisa831@gmail.com’, “name” = ‘user3’, “updated_at” = ‘2020-04-25 11:39:52’ WHERE “users”.”id” = 6 SAVE Save は SQL を実行する際にすべてのフィールドを含みます。フィールドを指定しなくても空にはなりません。 user := &models.User{Name: …