🌟

Ginのチュートリアルから派生させて、PostgreSQLへの保存まで

2022/12/31に公開

GolangのフレームワークであるGinを学習しようと思ったときのファーストステップとして、Googleが出しているGinのチュートリアルをやってみるのはオススメです。
https://go.dev/doc/tutorial/web-service-gin

しかし、上記の方法はメモリにデータを保持しているため、テーブルへの保存などには触れられていません。
なので、上記のチュートリアルから派生させて、ローカルで立てたPostgreSQLに対してのデータの保存を行ってみました。
その備忘録です。

リポジトリはこちら
https://github.com/10inoino/go-crud-demo

オニオンアーキテクチャも意識しながら書いてみたのですが、怪しいところもあるので、是非コメントでご教授いただけると幸いです。

SQL-Migrate

https://github.com/rubenv/sql-migrate

今回はORMapperとしてSQLBoilerを利用します。
SQLBoilerにはマイグレーション機能がついていないので、SQL-Migrateを利用して、マイグレーションを行ってから、SQLBoilerでの型定義を行います。

https://github.com/10inoino/go-crud-demo/blob/main/db/migrations/migration.sql
(チュートリアルでは、Priceはfloat64なので、少数も使えますが、今回はintにして少数を利用しない方針で実装しています。)

https://github.com/10inoino/go-crud-demo/blob/main/db/dbconfig.yml

DB接続に必要な環境変数は、docker-compose.ymlで定義しています。
https://github.com/10inoino/go-crud-demo/blob/main/.devcontainer/docker-compose.yml

上記を設定したら、SQL-Migrateをインストールして

go install github.com/rubenv/sql-migrate/...@v1.2.0

マイグレーションのコマンドを実行すると、テーブルが作成されます。

sql-migrate up

SQLBoiler

上記で説明した通り、ORMapperにはSQLBoilerを利用しました。
会社で利用しているのがGormなので、Gorm以外のORMapperを使ってみたいという安直な理由です。

設定用のtomlファイルを以下のように定義しました。
https://github.com/10inoino/go-crud-demo/blob/main/sqlboiler.toml

パラメータが少ないように思えますが、ドライバ名(上記だとpsql)を先頭に付けた環境変数は、SQLBoilerの設定パラメータとして扱われるので、主なテーブルの接続情報に関してはSQL-Migrateと同様、docker-compose.ymlに記載しています。
https://github.com/volatiletech/sqlboiler#database-driver-configuration

sqlboilerのコマンドを叩くと、SQL-Migrateで作成したテーブルの情報を元に構造体が作成されます。

sqlboiler psql

Domain

チュートリアルでは、アルバムの情報のCRUD操作を行っています。
なので、まずはアルバムのドメインを作成します。
https://github.com/10inoino/go-crud-demo/blob/main/src/domain/album.go

本来はNewAlbum内でバリデーションなどをやるべきですが、今回は時間の都合で省略してます。

Repository

オニオンアーキテクチャを意識して、リポジトリのインターフェースを実装してから、PostgreSQL用のリポジトリを書いていきます。

インターフェース
https://github.com/10inoino/go-crud-demo/blob/main/src/repository/interface/album_repository.go

PosgreSQL用のリポジトリ
https://github.com/10inoino/go-crud-demo/blob/main/src/repository/postgres/repository/album_repository.go

また、この時点でエラードメインも書きました。
https://github.com/10inoino/go-crud-demo/blob/main/src/domain/error.go

理由としては、リポジトリの独自定義のエラーをユースケース層に意識させたくなかったからです。
例としてFindByIdを挙げます。

func (repo *AlbumPostgresRepository) FindById(ctx *gin.Context, id string) (*domain.Album, error) {
	m, err := models.FindAlbum(ctx, repo.db, id)
	if err == sql.ErrNoRows {
		return nil, domain.NewNotFoundError("album not found")
	}
	if err != nil {
		return nil, domain.NewInvalidInputError("failed find album")
	}
	album, _ := domain.NewAlbum(m.ID, m.Title, m.Artist, m.Price)
	return album, nil
}

FindAlgumで、データが取得できなかった場合、err == sql.ErrNoRowsがtrueになるわけですが、sql.ErrNoRowsは永続層に閉じ込めておきたいハンドリングだったので、自分で独自のエラー型を定義して、リポジトリ層からはその独自のエラー型を返しています。

Usecase

今回は明確なユースケースが想定されているわけではありませんが、List、Get、Create、Update、Deleteをそれぞれユースケースと見立てて実装しました。

例としてCreateのユースケースを掲載しておきます。
https://github.com/10inoino/go-crud-demo/blob/main/src/usecase/album_create_usecase.go

Presentation

プレゼンテーション層で、APIリクエストをユースケース層に渡すマッピングを行います。
https://github.com/10inoino/go-crud-demo/blob/main/src/presentation/controller/album_controller.go

デモアプリなので、エラーの場合はすべてInternalServerErrorで返すようになっています。(サボりました。)

また、現在はCreateとUpdateのAPIリクエストの型がAlbum構造体と一緒なのですが、ドメイン構造や今後の開発を考えると、リクエストの型は別にすべきだろうと考え、リクエスト用の型を定義しています。
https://github.com/10inoino/go-crud-demo/blob/main/src/domain/request/album_request.go

また、 Ginのルーティングの関係で、引数が*gin.Contextだけの関数しかルーティングに渡せないので、プレゼンテーション層ではそれを意識して書いています。

Main.go

main.goで、今まで書いてきたそれぞれのレイヤーを定義しながら、RestAPIのルーティングに対応させていきます。
https://github.com/10inoino/go-crud-demo/blob/main/main.go

DIを考慮したコードを自動生成してくれるwireというツールが出ているので、実務ではこれを使うべきかなと思いますが、今回は自分の書いたコードを検証しながらAPIを作成していたので、自分で各レイヤーの構造体を定義しています。
https://github.com/google/wire

ここまで実装すると、APIとして実行できるようになります。

おわり

Ginのチュートリアルも手軽にGinの使い方がわかる、いいチュートリアルでしたが、そのチュートリアルをここまで深ぼって書いてみると、プロダクトを1から作るイメージが湧きました。
今後Ginに入門したいと思っている方は是非一度挑戦してみては如何でしょうか?

Discussion