前回の続きということでday2です
- TODOにUpdate, Deleteを追加
- Packageを分ける
CRUDなTODOになったと思います。
作成したコードはsourjp/go-practice/day2にあります。
構成
少ない内容であればMVCで分ける方がわかりやすいですね。
$ tree
.
├── Makefile
├── controllers
│ ├── router.go
│ └── todo.go
├── go.mod
├── go.sum
├── main.go
├── models
│ ├── db.go
│ └── todo.go
└── todo.sql
package models
todo.sql
CREATE TABLE IF NOT EXISTS todo (
id SERIAL NOT NULL PRIMARY KEY,
title VARCHAR(25),
message VARCHAR(255),
created_at TIMESTAMP,
updated_at TIMESTAMP,
finished_at TIMESTAMP
)
models/db.go
package models
import (
"database/sql"
"fmt"
)
func NewDB() (*sql.DB, error) {
// paramsはconfファイルからそのうち読めるようにします
var (
driver = "postgres"
host = "localhost"
user = "root"
password = "root"
dbname = "todo"
sslmode = "disable"
params = fmt.Sprintf("host=%s user=%s password=%s dbname=%s sslmode=%s", host, user, password, dbname, sslmode)
)
db, err := sql.Open(driver, params)
if err != nil {
return nil, err
}
if err := db.Ping(); err != nil {
return nil, err
}
return db, nil
}
models/todo.go
package models
import (
"database/sql"
"fmt"
"time"
)
type TODOList struct {
db *sql.DB
}
func NewTODOList(db *sql.DB) *TODOList {
return &TODOList{db: db}
}
type TODO struct {
ID int `db:"id" json:"id"`
Title string `db:"title" json:"title"`
Message string `db:"message" json:"message"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
// pointer型にすることでから文字判定ができるようにしomitemptyが聞くようになる
UpdatedAt *time.Time `db:"updated_at" json:"updated_at,omitempty"`
FinishedAt *time.Time `db:"finished_at" json:"finished_at,omitempty"`
}
func (tl *TODOList) Create(t TODO) error {
if t.CreatedAt.IsZero() {
t.CreatedAt = time.Now()
}
fmt.Println(t)
const sql = "INSERT INTO todo (title, message, created_at, finished_at) VALUES ($1, $2, $3, $4)"
_, err := tl.db.Exec(sql, t.Title, t.Message, t.CreatedAt, t.FinishedAt)
if err != nil {
return err
}
return nil
}
func (tl *TODOList) Get(id int) (TODO, error) {
var t TODO
const sql = "SELECT * FROM todo WHERE id = $1"
err := tl.db.QueryRow(sql, id).Scan(&t.ID, &t.Title, &t.Message, &t.CreatedAt, &t.UpdatedAt, &t.FinishedAt)
if err != nil {
return TODO{}, err
}
return t, nil
}
func (tl *TODOList) GetItems(limit int) ([]TODO, error) {
const sql = "SELECT * FROM todo ORDER BY id DESC LIMIT $1"
rows, err := tl.db.Query(sql, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var todos []TODO
for rows.Next() {
var t TODO
rows.Scan(&t.ID, &t.Title, &t.Message, &t.CreatedAt, &t.UpdatedAt, &t.FinishedAt)
todos = append(todos, t)
}
if err := rows.Err(); err != nil {
return nil, err
}
return todos, nil
}
func (tl *TODOList) Update(t TODO, id int) error {
const sql = "UPDATE todo SET title = $1, message = $2, updated_at = $3 WHERE id = $4"
// t.UpdatedAtにはpointerで渡す。もっとスッキリした書き方はないのかな?
updatedAt := time.Now()
t.UpdatedAt = &updatedAt
_, err := tl.db.Exec(sql, t.Title, t.Message, t.UpdatedAt, id)
if err != nil {
return err
}
return err
}
func (tl *TODOList) Delete(id int) error {
const sql = "DELETE FROM todo WHERE id = $1"
_, err := tl.db.Exec(sql, id)
if err != nil {
return err
}
return nil
}
package controllers
controllers/router.go
package controllers
import (
"log"
"github.com/gin-gonic/gin"
"github.com/sourjp/go-practice/day2/models"
)
func Router() {
r := gin.Default()
db, err := models.NewDB()
if err != nil {
// DBが読み込めなかったら起動して欲しくないのでFatal
log.Fatal(err)
}
v1 := r.Group("/api/v1/todo")
{
th := NewTODOHandler(models.NewTODOList(db))
v1.GET("/getitems", th.GetItems)
v1.GET("/get/:id", th.Get)
v1.POST("/create", th.Create)
v1.PUT("/update/:id", th.Update)
v1.DELETE("/delete/:id", th.Delete)
}
r.Run(":8080")
}
controllers/todo.go
package controllers
import (
"log"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/sourjp/go-practice/day2/models"
)
// resWithErr で errorをまとめるようにしたが、ちょっとダサい。
func resWithErr(c *gin.Context, code int, message interface{}) {
c.AbortWithStatusJSON(code, gin.H{"error": message})
}
func resNoErr(c *gin.Context, code int, message interface{}) {
c.JSON(code, gin.H{"status": message})
}
type TODOHandler struct {
tl *models.TODOList
}
func NewTODOHandler(tl *models.TODOList) *TODOHandler {
return &TODOHandler{tl: tl}
}
func (th *TODOHandler) GetItems(c *gin.Context) {
ls, ok := c.GetQuery("limit")
if !ok {
ls = "10"
}
li, err := strconv.Atoi(ls)
if err != nil {
log.Println(err)
resWithErr(c, http.StatusBadRequest, "failed to parse your query")
return
}
todos, err := th.tl.GetItems(li)
if err != nil {
log.Println(err)
resWithErr(c, http.StatusInternalServerError, "failed to get items")
return
}
if len(todos) == 0 {
resNoErr(c, http.StatusOK, "there is no todos. create your todo!")
return
}
c.JSON(http.StatusOK, todos)
}
func (th *TODOHandler) Get(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
log.Println(err)
resWithErr(c, http.StatusBadRequest, "failed to parse URI")
return
}
todo, err := th.tl.Get(id)
if err != nil {
log.Println(err)
resWithErr(c, http.StatusInternalServerError, "failed to get item")
return
}
c.JSON(http.StatusOK, todo)
}
func (th *TODOHandler) Create(c *gin.Context) {
var t models.TODO
if err := c.ShouldBindJSON(&t); err != nil {
log.Println(err)
resWithErr(c, http.StatusBadRequest, "failed to parse your request")
return
}
if err := th.tl.Create(t); err != nil {
log.Println(err)
resWithErr(c, http.StatusInternalServerError, "failed to create item")
return
}
resNoErr(c, http.StatusOK, "created")
}
func (th *TODOHandler) Update(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
log.Println(err)
resWithErr(c, http.StatusBadRequest, "failed to parse URI")
return
}
var t models.TODO
if err := c.ShouldBindJSON(&t); err != nil {
log.Println(err)
resWithErr(c, http.StatusBadRequest, "failed to parse your request")
return
}
if err := th.tl.Update(t, id); err != nil {
log.Println(err)
resWithErr(c, http.StatusInternalServerError, "failed to update item")
return
}
resNoErr(c, http.StatusOK, "updated")
}
func (th *TODOHandler) Delete(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
log.Println(err)
resWithErr(c, http.StatusBadRequest, "failed to parse URI")
return
}
if err := th.tl.Delete(id); err != nil {
log.Println(err)
resWithErr(c, http.StatusInternalServerError, "failed to delete item")
}
resNoErr(c, http.StatusOK, "deleted")
}
package main
main.go
package main
import (
_ "github.com/lib/pq"
"github.com/sourjp/go-practice/day2/controllers"
)
func main() {
controllers.Router()
}
テスト
$ make test
# 登録データなし
curl localhost:8080/api/v1/todo/getitems
{"status":"there is no todos. create your todo!"}
# 3つTODOを登録
curl -X POST localhost:8080/api/v1/todo/create -d '{"title": "yesterday", "message": "cooked"}'
{"status":"created"}
curl -X POST localhost:8080/api/v1/todo/create -d '{"title": "today", "message": "cook"}'
{"status":"created"}
curl -X POST localhost:8080/api/v1/todo/create -d '{"title": "tomorrow", "message": "will cook"}'
{"status":"created"}
# TODO #3を更新
curl localhost:8080/api/v1/todo/get/3
{"id":3,"title":"tomorrow","message":"will cook","created_at":"2020-08-22T16:55:22.235317Z"}
curl -X PUT localhost:8080/api/v1/todo/update/3 -d '{"title": "tomorrow", "message": "will not cook"}'
{"status":"updated"}
curl localhost:8080/api/v1/todo/get/3
{"id":3,"title":"tomorrow","message":"will not cook","created_at":"2020-08-22T16:55:22.235317Z","updated_at":"2020-08-22T16:55:22.310527Z"}
# TODO #1を削除
curl -X DELETE localhost:8080/api/v1/todo/delete/1
{"status":"deleted"}
# TODO の結果を確認
curl localhost:8080/api/v1/todo/getitems
[{"id":3,"title":"tomorrow","message":"will not cook","created_at":"2020-08-22T16:55:22.235317Z","updated_at":"2020-08-22T16:55:22.310527Z"},{"id":2,"title":"today","message":"cook","created_at":"2020-08-22T16:55:22.193637Z"}]
# TODOの表示数をqueryで変更
curl localhost:8080/api/v1/todo/getitems?limit=1
[{"id":3,"title":"tomorrow","message":"will not cook","created_at":"2020-08-22T16:55:22.235317Z","updated_at":"2020-08-22T16:55:22.310527Z"}]
todo=# select * from todo;
id | title | message | created_at | updated_at | finished_at
----+----------+---------------+----------------------------+----------------------------+-------------
2 | today | cook | 2020-08-22 16:55:22.193637 | |
3 | tomorrow | will not cook | 2020-08-22 16:55:22.235317 | 2020-08-22 16:55:22.310527 |
(2 rows)
まとめ
前回と比較するとここが組み込めたかと思います。
- TODOにUpdate, Deleteを追加
- Packageを分ける
次の課題はここら辺から抽出して組み込もうと思います。
- 設定をconfファイルから読み込むようにする
- Graceful shutdownを入れる
- 自作Errorを用意する
- Testを書く
- User機能(signup/login/logout)あたりを追加する
- Middleware - Authを追加する