はじめに
こんにちは。富士通の阿部です。こちらの記事は Fujitsu Advent Calendar 2024 の 23日目 の記事です。
本アドベントカレンダー内では、量子計算やFortranの記事など興味のある記事がたくさん出ていたので、後でじっくり読んでみようと思います。
わたしの記事では敷居を下げて「入門してみた」の話になります。よろしくお願いします。
概要
下記の要素に触れつつ簡単なTODOアプリを作成しました。
- sqlc
- CQRS・ES
- Huma
きっかけ
こちらの本を読んで
- DDD + CQRS・ESをやってみたくなった
- sqlcはGoでDB触る機会があればやってみたかった
- Humaはこの記事を書く前にちょっとバズっていて気になったので急遽
アプリ概要
簡単なタスク管理ツールになります
- データベース: postgreSQLをsqlcで利用
- Webページ: ビルトインのhttpパッケージのhttp.ServeMuxを利用
- API: http.ServeMuxをHumaでwrapして利用
- アーキテクチャ: CQRS・ESを利用してイベントストアにイベントを収集 + イベントをもとにTaskProjectionで現在のタスクの状態を反映
- UI: とりあえずで
いったんは勉強用で作ったものですが、あわよくば普段のタスク管理+TODO管理ツールとして使えるように更新していきたいです。
各要素の使い勝手
sqlc
sqlcではORMを覚える必要がなく、直接SQLを操作する感覚がシンプル感あって好みでした。
クエリ定義ファイル
-- name: UpdateTaskState :exec
UPDATE tasks SET status = $2
WHERE task_id = $1;
sqlcの利用コード
// postgresのconn作成
ctx := context.Background()
connString := "postgres://root:password@localhost:5432/appdb?sslmode=disable"
conn, err := pgx.Connect(ctx, connString)
if err != nil {
slog.Error("postgres connection error", slog.Any("err", err))
}
defer conn.Close(ctx)
// sqlcの利用
queries := db.New(conn)
queries.UpdateTaskState(ctx, db.UpdateTaskStateParams{
TaskID: uuid.New(),
Status: db.TaskStatus("hogehoge"),
);
実際のコーディングの流れとしては、定義ファイルに-- name: UpdateTaskState :exec
のように記述して、go generate ./...
した後、コード上で、queries.
まで打って補完でどんどん埋めていくだけなので手軽でした。
今回はまだ簡単なクエリを少量しか書けていないので、複雑なクエリを書いてみたいです。クエリをブクブク増やしてゆく運用と、汎用的なクエリにとどめてアプリ側で頑張る運用のどちらが良い塩梅かなど見ていきたいです。
Huma
Humaを利用することで、Goのhttp.ServeMux
を拡張しながら、APIエンドポイントを定義し、OpenAPI 3.0/3.1仕様のドキュメントを自動生成できました。コードだとこの辺
ビルトインのハンドラ関数は、
type RequestHoge struct {...}
type ResponseHoge struct {...}
func HogeHandler(w http.ResponseWriter, r *http.Request) {
var req RequestHoge
json.NewDecoder(r.Body).Decode(&req)
...
resp := ...
json.NewEncoder(w).Encode(resp)
}
のような形式で、I/Oストリームを意識したデータハンドリングが必要です。(Goでストリームを上手に使っているコードはカッコよくて結構好きです。)
一方、Humaでは
type RequestHoge struct {...}
type ResponseHoge struct {...}
func HogeHandler(ctx context.Context, req *RequestHoge) (*ResponseHoge, error) {
...
}
という形式で、普通の関数に似ていて親しみやすいです。
ハンドラ関数をrouterに登録部分は、ビルトインのhttp.ServeMuxの場合は、
mux.HandlerFunc("POST /tasks/{taskId}", HogeHandler)
の形ですが、Humaでは、
api := humago.New(mux, huma.DefaultConfig("TaskTODO", "1.0.0")
huma.POST(api, "/tasks/{taskId}", HogeHandler)
という形であまり変わらないです。Go 1.22でビルトインのmuxも使い勝手がかなり良くなっているので、正直ビルトインのほうが好みです。(http.ServeMuxで利用できるpathのパターン)
それよりもHumaを使いたかった理由として、open API 3.0/3.1準拠のドキュメントページが/doc
に自動生成されるというのがあります。
ドキュメントの内容は、以下のようにRequest/Responseに利用する構造体のタグに記載します。
type Task struct {
Name string `json:"name" example:"空き缶を片付ける" doc:"task name"`
Id uuid.UUID `json:"task_id" example:"ea087856-822f-4db3-a9c7-23ce3dd337b3" doc:"uuid"`
Status string `json:"status" enum:"pending,doing,completed,cancelled" example:"pending" doc:"task status"`
}
type ResponseTasks struct {
Body []Task
}
(ここのコードはリクエストボディしかRequestの構造に含まれていないが、クエリパラメータ、パスパラメータなどもRequest内で定義していく)
ここのタグを丁寧に書いていくことで、OpenAPIのドキュメントが充実していきつつ、内容によっては、enumとかformat: "uuid"とかを書いてあげることでハンドラー関数の処理の手前でバリデーションが実施されます。
あまり手をかけなくてもサマになるので、書いていて楽しかったです。もっと内容を充実させたいなーと思いながら書いていました。
Humaの利用もチュートリアルのレベルなので、HumaでできることはなるべくHumaにやらせる方針で使っていきたいです。
CQRS・ES
CQRSは今までメリットがわからず勉強できていませんでしたが、ES(Event Sourcing)のことを知ったことと、丁度自分のタスク管理方法に悩んでいて、自分のタスクやTODOのこなし具合を統計とってグラフで見たりできたら楽しいだろうな、と思っていたのでそのあたりのピースがハマって入門しよう、ということになりました。
今回のアプリでは、イベントストアテーブル + タスクのプロジェクション用テーブル + チェックポイントのテーブル、という構成でやっています。
実装するだけした状態で、まだあまり頭の中が整理できていないので、整理できたら構成などの詳細を追記していこうと思っています。
今のところの感想としては、実装が重いけど拡張性があるという雰囲気です。
特に、Get側の利用用途に応じたテーブルと投影エンジンを作ることで、共通の情報源から複数のデータを表現できるというのは、監査用・統計用・リソース監視用などの機能を独立性高く管理でき、イベントをPOSTしていく側(コマンド側)の処理もそれらの要件に左右されず安定させられそうで、便利に感じます。
今後の展望
- DDD(ドメイン駆動設計の本を読んだのに、まだドメイン駆動設計していないので)
- テスト(今のところ0個なので)
- 今回の記事では触れませんでしたが、slogとgolang-migrateも入門中なので、いろいろ機能を使ってみたいです。
最後に
最後まで見ていただきありがとうございました。気になるところがあればご連絡ください!