前回 → https://trap.jp/post/1517/
次回 → (フロント編後編) https://trap.jp/post/1497/
Github → https://github.com/Irori235/ToDoList-Server/tree/day-4
21Bのいろりです。おいす〜。
この記事はサーバー編4日目です。今日で最終回を迎え、ToDoリストが完成します!
今日やること
まずDay-3にやったことを復習しましょう。
前回やったことの復習
- データベース(DB) 設定やルーティングの設定をする
- Goを書き、タスク一覧を取得するAPIを実装する
- Dockerを起動し、正常に動作するか確かめる
これを踏まえて、今日やることはこちらです。
今日やること
- 残りの機能を実装し、ToDoリストを完成させる
- 起動し、正常に動くか確認する
- 今後の勉強の流れを知る
Githubのここに今日の完成品のコードがあります。ここをみながら進めましょう。
※ .gitignore(gitを使うときに書く) や README.md(説明書) はGoの動作に関係しないため、無視してください。
POST (api/tasks)
前回のGET
に引き続き、POST
を実装します。
APIを追記
router/router.go
にAPIを追記します
router/router.go
// APIを書く場所
e.GET("/api/tasks", GetTasksHandler)
e.POST("/api/tasks", AddTaskHandler) //これを追記
追記後の router/router.go
package router
import (
"os"
"github.com/labstack/echo/v4/middleware"
_ "net/http"
"github.com/labstack/echo/v4"
)
//Routingを設定する関数 引数はecho.echo型であり、戻り値はerror型
func SetRouter(e *echo.Echo) error {
// 諸々の設定(*1)
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: "${time_rfc3339_nano} ${host} ${method} ${uri} ${status} ${header}\n",
Output: os.Stdout,
}))
e.Use(middleware.Recover())
e.Use(middleware.CORS())
// APIを書く場所
e.GET("/api/tasks", GetTasksHandler)
e.POST("/api/tasks", AddTaskHandler)
// 8000番のポートを開く(*2)
err := e.Start(":8000")
return err
}
AddTaskHandler
に赤線(エラー)が引いてあると思います。
カーソルをあてると、「undeclared name: AddTaskHandler」と書いてあります。
これを和訳すると「AddTaskHandlerなんてものは定義されていません💢 」になりますね。
ですから、AddTaskHandlerを定義していきます。
エラーの解決策
よく見ればエラーの原因が書かれているのに、 見落として悩み続けていることがよくあります。
これは問題文を読み飛ばして、解答形式を間違えるのと同じで、なかなか無くなりません。
エラーを意識的に読む習慣をつけましょう !
それでもわからない場合は、エラー文をコピペしてググると大抵のことは解決します。(マジ)
AddTaskHandlerを実装
関数AddTaskHandler を実装します。
POST
はbody にタスクの名前が入って送信されてくるのでした ([Day-2 APIの仕様策定])
その際にRequsetTask構造体を定義します。そのままだと長いため、ReqTaskとします。
関数 GetTaskhandlerの下に、
router/task.go
// ReqTask型は文字列のNameをパラメーターとして持つ
type ReqTask struct {
Name string `json:"name"`
}
json:"name"
とは jsonデータを代入するための識別子です。と、言われてもピンとこないと思うので、関数 AddTaskHandlerの実装を通して理解しましょう。
router/task.go
// 関数 AddTaskHandler は引数がecho.Context型で、戻り値はerror型である
func AddTaskHandler(c echo.Context) error {
// 空のReqTaskである、reqを定義
var req ReqTask
// bodyのjsonファイルをbind(*1)
err := c.Bind(&req)
// エラーハンドリング(day-3のGetTaskHandlerを参照)
// StatusBadRequestを返す
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
}
// 空のmodel(package)のTaskである、taskを定義
var task *model.Task
// model(package)のAddTask関数を実行し、戻り値をtask,errと定義
task, err = model.AddTask(req.Name)
// エラーハンドリング
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
}
// StastsOK と 追加されたtaskを返す
return c.JSON(http.StatusOK, task)
}
を追記します。
追記後の router/task.go
package router
// 使用するライブラリをimport
import (
"ToDoList-Server/model"
"net/http"
"github.com/labstack/echo/v4"
)
// ReqTask型は文字列のNameをパラメーターとして持つ
type ReqTask struct {
Name string `json:"name"`
}
// 関数 GetTasksHandlerは引数がecho.Context型のc で、戻り値はerror型である
func GetTasksHandler(c echo.Context) error {
// model(package)の関数GetTasksを実行し、戻り値をtasks,errと定義する
tasks, err := model.GetTasks()
// errが空でない時は StatusBadRequest(*5) を返す
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
}
// StasusOK と tasksを返す
return c.JSON(http.StatusOK, tasks)
}
// 関数 AddTaskHandler は引数がecho.Context型で、戻り値はerror型である
func AddTaskHandler(c echo.Context) error {
// 空のReqTaskである、reqを定義
var req ReqTask
// bodyのjsonファイルをbind(*1)
err := c.Bind(&req)
// エラーハンドリング(day-3のGetTaskHandlerを参照)
// StatusBadRequestを返す
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
}
// 空のmodel(package)のTaskである、taskを定義
var task *model.Task
// model(package)のAddTask関数を実行し、戻り値をtask,errと定義
task, err = model.AddTask(req.Name)
// エラーハンドリング
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
}
// StastsOK と 追加されたtaskを返す
return c.JSON(http.StatusOK, task)
}
AddTaskの実装
関数 GetTasksの下に、
model/task.go
// 関数 AddTask は引数がstring型のnameで、戻り値はTaskのポインターとerror型である (*1)
func AddTask(name string) (*Task, error) {
// 新たなuuidを生成し、これをid、成否をerrとする(*2)
id, err := uuid.NewUUID()
if err != nil {
return nil, err
}
// ID,Name,Finishedにid,name,false を代入したTask型のtaskを定義
task := Task{
ID: id,
Name: name,
Finished: false,
}
// taskをDBのTaskテーブルに追加。その成否を(ry
err = db.Create(&task).Error
// taskのポインタ と errを返す
return &task, err
}
*1 ポインタは難しいので、 とりあえずこういう書き方だと動くんだ〜でOK。詳しい解説はここがわかりやすい。
[とってもやさしいGo言語入門]
*2 NewUUIDはライブラリgoogle.uuid
の関数。関数めいの通り、新しいUUIDを生成する。
動作確認
プロジェクトのルートで
$ docker compose up -d --build
serverコンテナに入り、
go run main.go
Echoが起動したことを確認します。
Postmanを開き、POST
http://localhost:8000/api/tasks
を設定し、body →raw →JSON を選択して、
{
"name": "クリップを買う"
}
をセットして叩きます(Send)。
serverのコンテナに<時刻> localhost:8000 POST /api/tasks 200
のログが表示され、
レスポンスで、新しく作られたtask型がjsonで返されて入れば成功です。
phpMyAdmin
DBはエクセルっぽい形をしていると書きました。それならエクセルっぽくDBの中身を表示するツールがないものか? と考えるでしょう。そういったツールの一つがphpmyAdminです。
見た目の古くささとは裏腹に、現在も継続的にアップデートされて、よく使われているツールです。
ブラウザで http://localhost:4040
にアクセスするとphpMyAdminが開きます。
左側のタブからTodolist →tasks を開くと、先ほど送信したタスクがDB上に登録してあるのがわかります。
動作確認が終わったら、
$ docker compose down
でコンテナを停止します。
PUT (api/tasks/:taskID)
続いて、PUT
とDELETE
もゴリゴリ実装していきましょう。
router/router.go
// APIを書く場所
e.GET("/api/tasks", GetTasksHandler)
e.POST("/api/tasks", AddTaskHandler)
e.PUT("/api/tasks/:taskID", ChangeFinishedTaskHandler) //これを追加
関数 AddTaskHandlerの下に、
router/task.go
func ChangeFinishedTaskHandler(c echo.Context) error {
// taskIDのパスパラメータ(string型)を取得し、uuid型に変換。その値をtaskID、成否をerrとする
taskID, err := uuid.Parse(c.Param("taskID"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
}
// 関数 ChangeFinishedTaskを実行、戻り値をerrに代入する(errを更新した)
err = model.ChangeFinishedTask(taskID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
}
return c.NoContent(http.StatusOK)
}
ここでもgoogle/uuid
を使ったため(uuid.Parse
)、忘れずにimportを追記しておきましょう。
router/task.go
import (
"ToDoList-Server/model"
"net/http"
"github.com/google/uuid" //これを追記
"github.com/labstack/echo/v4"
)
続いて 関数AddTaskの下に、
model/task.go
// 関数 ChangeFinishedTaskの引数はuuid.UUID型のtaskIDで、戻り値はerror型である
func ChangeFinishedTask(taskID uuid.UUID) error {
// DBのTaskテーブルからtaskIDと一致するidを探し、そのFinishedをtureにする(*3)
err := db.Model(&Task{}).Where("id = ?", taskID).Update("finished", true).Error
return err
}
(*3) Excelのクエリと似ていますね ! DBに格納されたデータを操作することをCRUDといいます。自分で新しいアプリを作る際に、DB上のデータ操作で困ったときは、GORMのドキュメントのCRUD Interface
のところを覗いてみるといいでしょう。
[GORMガイド |GORM]
DELETE (api/tasks/:taskID)
今までの応用で全て書けます。是非やっていきましょう !
router/router.go
// APIを書く場所
e.GET("/api/tasks", GetTasksHandler)
e.POST("/api/tasks", AddTaskHandler)
e.PUT("/api/tasks/:taskID", ChangeFinishedTaskHandler)
e.DELETE("/api/tasks/:taskID", DeleteTaskHandler) //これを追加
router/task.go
func DeleteTaskHandler(c echo.Context) error {
taskID, err := uuid.Parse(c.Param("taskID"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
}
err = model.DeleteTask(taskID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
}
return c.NoContent(http.StatusOK)
}
model/task.go
func DeleteTask(taskID uuid.UUID) error {
// DBのTaskテーブルからtaskIDと一致するidを探し、そのタスクを削除する
err := db.Where("id = ?", taskID).Delete(&Task{}).Error
return err
}
動作確認
前回と同様にします。ファイルの変更を保存し、Dockerコンテナを起動して、serverコンテナに入りgo run main.go
を打ちます。Echoが立ち上がったことを確認します。
GET
Postmanを開きGET
を叩いて、前回登録したタスク「クリップを買う」が返されることを確認します。
POST
次に、タスク「デスク整理」をセットして、POST
します。
phpMyAdminに、「デスク整理」が登録されていることを確認し、タスク「デスク整理」のidをコピーします。
PUT
このid をhttp://localhost:8000/api/tasks/:taskId
の:taskId
の部分にペーストして、PUT
を叩きます。:taskId
のようにして渡す値を、パスパラメーターと言います。
phpMyAdminでタスク「デスク整理」のfinished
が 1 (true)になっていることを確認します。
DELETE
同じようにして、DELETE
を叩きます。
phpMyAdminで確認します。
動作確認が終わったら、コンテナを停止します。
リファクタリング
リファクタリングとは、コードの挙動を変えずに、コードを整理することです。
router/router.go
を見てみましょう。APIのパスの指定が少々煩雑です。
全て api/tasks
で始まるのに、毎回書いているのは読みにくいですね。
router/router.go
// APIを書く場所
e.GET("/api/tasks", GetTasksHandler)
e.POST("/api/tasks", AddTaskHandler)
e.PUT("/api/tasks/:taskID", ChangeFinishedTaskHandler)
e.DELETE("/api/tasks/:taskID", DeleteTaskHandler)
を整理して、このように書くことができます。
// APIを書く場所
api := e.Group("/api")
{
apiTasks := api.Group("/tasks")
{
apiTasks.GET("", GetTasksHandler)
apiTasks.POST("", AddTaskHandler)
apiTasks.PUT("/:taskID", ChangeFinishedTaskHandler)
apiTasks.DELETE("/:taskID", DeleteTaskHandler)
}
}
動作確認をしましょう !
今後の勉強について
4日間お疲れ様でした 🎉
今回やったことは、実はtraP内の「Webエンジニアになろう講習会」(通称、なろう) という講習会の最終課題です。なろう講習会は、脱落者が非常に多いことで有名です (私も脱落した)。
ですから、最後まで完走できた人は誇りを持っていいと思います。できなかったとしても、Web開発の雰囲気を体感していただけたら幸いです。
サーバーサイドでは、今回取り上げたGoの知識以外でも
- フレームワーク, ライブラリ(echo, GORM)
- RDBMS(MySQL, MariaDB),NoSQL(MongoDB, DynamoDB)
- アーキテクチャ(DDD, クリーンアーキテクチャ)
- VPS(conoha), クラウド(AWS, GCP)
- 運用(Github Actions, dependabot, Ansible)
- 監視(Grafana)
などなど必要とされる知識は多岐に渡ります。いっぺんに勉強することは難しいので、自分でWebアプリを作りながらその都度調べていくのがいいのかなと思います。
また、Goについて深く知りたくなった場合には、[スターティングGo言語]が、よくおすすめされています。他に、ライブラリを一切使わずにWebアプリを作るのも非常に勉強になると言われています。これは[Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る (impress top gear)]を読むといいでしょう。
初心者を脱してきた方には、[みんなのGo言語]がおすすめです。
Web開発に興味が出てきたら、ぜひtraPのSysAd班で一緒に勉強しましょう ! それでは〜
謝辞
この記事を書くにあたって、mehm8128君、asari君、kounosuke君と先輩方にたくさんのアドバイスをいただきました。本当にありがとうございます。
明日は @logica さんです。楽しみ〜 !