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] Goを始めたらまずはioパッケージを知るべし

Goを書いているとio.writerとio.readerを扱うケースが頻繁に出てきますが、これはioパッケージが多くの他のパッケージのインターフェースになっているからなのでioパッケージを知っておくことで開発が楽になります。 参考書籍 本書はGoのio.Writer、io.Readerからはじまりシステムの深いところまで丁寧に説明されているのでとてもおすすめです。ioパッケージから始まっているのも納得です。Webで無料で見れますし書籍版、PDF版で購入も可能です。 Web版 https://ascii.jp/elem/000/001/235/1235262/ 書籍版 Goならわかるシステムプログラミング ioパッケージのインターフェース一覧 ioパッケージのインターフェース一覧です。Goのインターフェースの実装は、明示的にインターフェースを明示的にimplementsせず、インターフェースを満たしていたらimplementsしていることになります。 Reader(インターフェース)Writer(インターフェース)Seeker(インターフェース)Closer(インターフェース)ReadWriter(複合インターフェース)ReadCloser(複合インターフェース)WriteCloser(複合インターフェース)ReadSeeker(複合インターフェース)WriteSeeker(複合インターフェース)ReadWriteCloser(複合インターフェース)ReadWriteSeeker(複合インターフェース)ReaderFrom(インターフェース)WriterTo(インターフェース)ReaderAt(インターフェース)WriterAt(インターフェース)ByteReader(インターフェース)ByteScanner(インターフェース)ByteWriter(インターフェース)RuneScanner(インターフェース)StringWriter(インターフェース) 複合インターフェース表 Goではインターフェースにインターフェースを食わせることができ、ioパッケージで作られている複合インターフェースは以下となります。 インターフェースio.Readerio.Writerio.Seekerio.Closerio.ReadWriter◯◯  io.ReadSeeker◯ ◯ io.ReadCloser◯  ◯io.WriteSeeker ◯◯ io.WriteCloser ◯ ◯io.ReadWriteSeeker◯◯◯ io.ReadWriteCloser◯◯ ◯ インターフェースを満たしている一覧を確認する方法 以下のコマンドを叩くと対象のインターフェースを満たしているものの一覧が確認できます。 $ GOPATH=/ godoc -http “:6060” -analysis type ## 実行後以下のURLにアクセスすると`io`パッケージが確認できる http://localhost:6060/pkg/io/ Readerのimplements一覧 テストも含んでいますが沢山あります pointer type *archive/tar.Reader implements Readerpointer type *archive/tar.regFileReader implements Readerpointer type *archive/tar.sparseFileReader implements Readerpointer type *archive/tar.testFile implements Readerpointer type *archive/zip.checksumReader implements Readerpointer type *archive/zip.pooledFlateReader implements Readerpointer type *bufio.Reader implements Readerpointer type *bufio_test.StringReader implements Readerpointer type *bufio_test.emptyThenNonEmptyReader implements Readerpointer type *bufio_test.errorThenGoodReader implements Readerpointer type *bufio_test.negativeReader implements Readerpointer type *bufio_test.rot13Reader implements Readerpointer type *bufio_test.scriptedReader implements Readerpointer type *bufio_test.slowReader implements Readerpointer type *bufio_test.testReader implements Readerpointer type *bytes.Buffer implements Readerpointer type *bytes.Reader implements Readerpointer type *bytes_test.negativeReader implements Readerpointer …

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 // …

Go言語 ORMライブラリ GORMの使い方

Go言語 ORMライブラリのGORMの簡単な使い方を確認してみました。また、公式ドキュメントにしっかりと使い方が書いてありますので基本的にはそちらを参考にしてもらえればと思います(すべてではないですが日本語訳もされています)。その上でクイックスタートを元に簡単な使い方と挙動を確認してみます。 http://gorm.io/ja_JP/docs/ インストール 以下のコマンドでインストールできます。 go get -u github.com/jinzhu/gorm クイックスタート 公式ドキュメントにあるクイックスタートを実行してみました。DBだけsqliteではなくmysqlに変更しています。 package main import ( “github.com/jinzhu/gorm” // _ “github.com/jinzhu/gorm/dialects/sqlite” _ “github.com/jinzhu/gorm/dialects/mysql” ) type Product struct { gorm.Model Code string Price uint } func main() { // db, err := gorm.Open(“sqlite3”, “test.db”) db, err := gorm.Open(“mysql”, “gorm:gorm@/sandbox?charset=utf8mb4&parseTime=True&loc=Local”) if err != nil { panic(“データベースへの接続に失敗しました”) } defer db.Close() // スキーマのマイグレーション db.AutoMigrate(&Product{}) // Create db.Create(&Product{Code: “L1212”, Price: 1000}) // Read var product Product db.First(&product, 1) // idが1の製品を探します db.First(&product, “code = ?”, “L1212”) // codeがL1212の製品を探します // Update – 製品価格を2,000に更新します db.Model(&product).Update(“Price”, 2000) // Delete – 製品を削除します db.Delete(&product) } 実行してみるとproductsテーブルが作成され、以下のカラムとレコードができました。structでは宣言していない、id、created_at、updated_at、deleted_atカラムができ、deleted_atに日付が入りソフトデリートが行われています。 go run main.go gorm.Model gorm.Modelを宣言するとid、created_at、updated_at、deleted_atカラムが自動的に注入されます。また,deleted_atカラムがある場合、Deleteはソフトデリートになります。 参考: http://gorm.io/ja_JP/docs/conventions.html …

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: …

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

前回の「Go言語 GORM+GinでTODOリストを作ってみた」に続いて「GORM+Gin」でTODOリストのAPIを作ってみました。ソースコードは前回からの差分だけを記載しています。できたものは下記URLから確認できます。 http://sandbox.taisablog.com/api/v1/ GinのGithub 事前情報 Webフレームワーク:Gin (https://github.com/gin-gonic/gin) ORM:GORM (https://gorm.io/docs) DB:MySQL ルーティングは今回はAPIなので以下としました。モデルをtasksにすればよかったと思いましたが一旦このままにしておきます。 [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] PUT /todo/:id –> main.main.func4 (3 handlers) // 編集 [GIN-debug] DELETE /todo/:id –> main.main.func5 (3 handlers) // 削除 ディレクトリ構成 . ├── api │   └── v1 │   └── todo.go ├── controllers │   └── todo.go ├── db │   └── db.go ├── main.go ├── models │   └── todo.go ├── router    └── router.go router.go router.goにr.Group(“/api/v1”)のAPI用のグループを追加してルーティングを追加しました。 package router import ( “github.com/gin-contrib/cors” “github.com/gin-gonic/gin” “github.com/jinzhu/gorm” v1 “github.com/taisa831/sandbox-gin/api/v1” “github.com/taisa831/sandbox-gin/controllers” …