やること
GoのwebフレームワークEchoになれるために練習するのが目的です。値をトグルするとは言っても、データベースのbool値をtrue<=>falseにする場合と、データベースのレコードをInsertしたりDeleteする場合(中間テーブルを使ってお気に入り機能を作るとか)があると思う。今回は、BoolTogglerモデルとして前者の実装をした後、UserモデルにBoolTogglerに対するのお気に入り機能をつけて後者を実装して行きます。(シュールで実用性に欠けるアプリケーションですが練習なので気にしないことにします...)
GitHubにてコード全体を乗せています。
DBのbool値をトグルするAPI
models
この記事の本題1です。モデルのbool型の列の値のTrue<=>Falseをトグルします。
こんなモデルを定義してマイグレーションしました。
models/bool_toggler.go
package models
import (
"github.com/jinzhu/gorm"
)
type BoolToggler struct {
// type gorm.Model struct {
// ID uint `gorm:"primaryKey"`
// CreatedAt time.Time
// UpdatedAt time.Time
// DeletedAt gorm.DeletedAt `gorm:"index"`
// }
gorm.Model
Toggler bool `json:"toggler"`
}
Togglerという単純なbool値をもつ構造体です。api部分を実装します。
APIの実装
web/api/toggle_bool_toggler.go
package api
import (
"github.com/labstack/echo"
"github.com/valyala/fasthttp"
"strconv"
"hello/models"
"hello/middlewares"
)
func ToggleBoolToggler() echo.HandlerFunc {
return func(c echo.Context) error {
// DB接続ミドルウェア
dbs := c.Get("dbs").(*middlewares.DatabaseClient)
// パスパラメータはstringなのでuintに変換する
intId, _ := strconv.Atoi(c.Param("id"))
uintId := uint(intId)
boolToggler := models.BoolToggler{}
if dbs.DB.First(&boolToggler, uintId).RecordNotFound() {
// idの指定が誤っている場合はステータスコード404を返す。
return c.JSON(fasthttp.StatusNotFound, "指定したidのboolTogglerが見つかりませんでした。")
} else {
// bool値を反転させて保存する
boolToggler.Toggler = !boolToggler.Toggler
dbs.DB.Save(&boolToggler)
return c.JSON(fasthttp.StatusOK, boolToggler)
}
}
}
現在設定されているTogglerを返すエンドポイント
web/api/get_bool_toggler.go
package api
import (
"github.com/labstack/echo"
"github.com/valyala/fasthttp"
"hello/models"
"hello/middlewares"
"strconv"
)
func GetBoolToggler() echo.HandlerFunc {
return func(c echo.Context) error {
dbs := c.Get("dbs").(*middlewares.DatabaseClient)
intId, _ := strconv.Atoi(c.Param("id"))
uintId := uint(intId)
boolToggler := models.BoolToggler{}
if dbs.DB.First(&boolToggler, uintId).RecordNotFound() {
return c.JSON(fasthttp.StatusNotFound, "指定したidのboolTogglerが見つかりませんでした。")
} else {
return c.JSON(fasthttp.StatusOK, boolToggler.Toggler)
}
}
}
基本的にはさっきと同じです。データを更新する代わりにブール値だけを取り出してレスポンスに入れています。
routes/api.go
func Init(e *echo.Echo) {
g := e.Group("/api")
{
g.PUT("/bool_toggler/:id/toggle", api.ToggleBoolToggler())
g.GET("/bool_toggler/:id", api.GetBoolToggler())
}
}
ここまで動作ををテストしてみます。
curl http://localhost:8080/api/bool_toggler/1
>> false
curl -XPUT http://localhost:8080/api/bool_toggler/1/toggle
>> {"ID":1,"CreatedAt":"2020-10-05T14:54:27Z","UpdatedAt":"2020-10-07T10:49:12.1435735Z","DeletedAt":null,"toggler":true}
curl http://localhost:8080/api/bool_toggler/1
>> true
// 未登録データ
curl http://localhost:8080/api/bool_toggler/3
>> "指定したidのboolTogglerが見つかりませんでした。
といった感じでうまく動作していることが確認できました。
お気に入り状態をトグルするAPI
models
中間テーブルで関連を表現するデータにおいて値をDeleteしたり、insertしたりするAPIを実装していきます。
単純なUserモデルを作るところから始めてきます。
中間テーブルによってリレーショナルな多対多なデータを単純なアプリケーションとして実装するのが目的なので、認証機能は作りません。とにかくシンプルにタイムスタンプと名前だけのモデルです。同様にマイグレーションします。
GORMで多対多のデータベースを作るには、https://gorm.io/ja_JP/docs/many_to_many.html
models/user.go
package models
import (
"github.com/jinzhu/gorm"
)
type User struct {
gorm.Model
name string `json:"name"`
// Favoritesという名前でbool_togglerを格納します
Favorites []*BoolToggler `gorm:"many2many:user_favorite_togglers;"`
}
APIの実装
先にURI設計ですが認証機能がないため、
/api/favorite/users/:user_id/bool_togglers/:toggler_id
というURIで複数のパスパラメータからそれぞれの実体を参照することが必要だということがわかります。以下API部分ですがかなり複雑になってきたのでコメントを多めに残しました
web/api/toggle_favorite_bool_toggler.go
package api
import (
"github.com/labstack/echo"
"github.com/valyala/fasthttp"
"strconv"
"hello/models"
"hello/middlewares"
)
func ToggleFavoriteToggler() echo.HandlerFunc {
return func(c echo.Context) error {
// response用のJSONを作る構造体
type Response struct {
UserId uint
BoolTogglerId uint
Favorite bool
}
// DB接続
dbs := c.Get("dbs").(*middlewares.DatabaseClient)
// パスパラメータをuintに変換する
intUserId, _ := strconv.Atoi(c.Param("user_id"))
uintUserId := uint(intUserId)
intTogglerId, _ := strconv.Atoi(c.Param("toggler_id"))
uintTogglerId := uint(intTogglerId)
// Response構造体をインスタンス化
var resJSON Response
resJSON.UserId = uintUserId
resJSON.BoolTogglerId = uintTogglerId
// AppendするときにIDを指定したboolTogglerを渡す
var boolToggler models.BoolToggler
boolToggler.ID = uintTogglerId
// Preloadでuserのリレーションを有効化してselect
user := &models.User{}
dbs.DB.Preload("Favorites", "bool_toggler_id = ?", uintTogglerId).
Find(&user, uintUserId)
// まだお気に入りされていなかった場合は、新しいレコードを追加
if len(user.Favorites) < 1 {
dbs.DB.Model(&user).Association("Favorites").Append(&boolToggler)
resJSON.Favorite = true
// お気に入り済みだった場合は、既存のレコードを削除
} else {
dbs.DB.Model(&user).Association("Favorites").Delete(&boolToggler)
resJSON.Favorite = false
}
return c.JSON(fasthttp.StatusOK, resJSON)
}
}
routes/api.go
g.POST("/favorite/users/:user_id/bool_togglers/:toggler_id", api.ToggleFavoriteToggler()) //追記
一応curlしてみるとちゃんと値をトグルしています。
curl -XPOST http://localhost:8080/api/favorite/users/1/bool_togglers/1
{"UserId":1,"BoolTogglerId":1,"Favorite":false}
/go/src/app # curl -XPOST http://localhost:8080/api/favorite/users/1/bool_togglers/1
{"UserId":1,"BoolTogglerId":1,"Favorite":true}
さらに、mysqlのテーブルも確認すると意図した挙動を確認できました。
mysql> select * from user_favorite_togglers;
+---------+-----------------+
| user_id | bool_toggler_id |
+---------+-----------------+
| 1 | 1 |
+---------+-----------------+
1 row in set (0.00 sec)
// アクセス後
mysql> select * from user_favorite_togglers;
Empty set (0.00 sec)
user情報を返すエンドポイント
web/api/show_user.go
package api
import (
"github.com/labstack/echo"
"github.com/valyala/fasthttp"
"hello/models"
"hello/middlewares"
"strconv"
)
func ShowUserInfo() echo.HandlerFunc {
return func(c echo.Context) error {
type ResToggler struct {
BoolTogglerId uint
Toggler bool
}
type Response struct {
UserId uint
Name string
Favorites []ResToggler
}
dbs := c.Get("dbs").(*middlewares.DatabaseClient)
// パスパラメータをuintに変換する
intUserId, _ := strconv.Atoi(c.Param("id"))
uintUserId := uint(intUserId)
// Preloadでuserのリレーションを有効化してselect
user := models.User{}
dbs.DB.Preload("Favorites").Find(&user, uintUserId)
var resJSON Response
resJSON.UserId = user.ID
resJSON.Name = user.Name
for _, v := range user.Favorites {
toggler := ResToggler{
BoolTogglerId: v.ID,
Toggler: v.Toggler,
}
resJSON.Favorites = append(resJSON.Favorites, toggler)
}
return c.JSON(fasthttp.StatusOK, resJSON)
}
}
Response用の構造体を定義しておくと、そのままJSONとして渡せるのがEchoフレームワークのいいところですね。
結果はこんな構造体を返します。
curl http://localhost:8080/api/user/1
{"UserId":1,"Name":"test_user01","Favorites":[{"BoolTogglerId":1,"Toggler":false}]}