[Go] 複数モジュールから参照するテーブルのモデルを共通化
複数のアプリケーションから呼ば得る Go のパッケージを別 module として切り出すことで、コードの多重管理を防ぐことができました。コードの共通化のアプローチとしては汎用的だと思ったので紹介します。
背景
- バックエンドには Hasura GraphQL Engine を採用している関係で、マイグレーション管理は Hasura で行っている
- Hasura のロジック拡張に Remote Schemas を採用 (Go + 99designs/gqlgen)
- Hasura の構成ファイルと Remote Schemas は同じリポジトリで管理
- Remote Schemas では ORM に GORM を使用
- テーブルのモデルの生成には smallnest/gen を使用
- DB のスキーマからいい感じに構造体を生成してくれます
# 実際のディレクトリ構成とは若干異なりますが、わかりやすさのために簡略化しています
.
├── external # Hasura の Remote Schemas になっている GraphQL サーバ
│ ├── models # DB モデル。gen で生成
│ │ ├── templates
│ │ └── gen
│ │ ├── hoge_table1.go
│ │ ├── hoge_table2.go
│ │ └── hoge_table3.go
│ # 途中省略
│ ├── go.mod
│ ├── go.sum
│ └── main.go
├── hasura # Hasura の構成ファイル。Go のプロジェクトではない
「Hasura / Remote Schemas って何」という方向けに解説記事を貼っておきます。わからなくても本記事の趣旨はつかめると思います。
やりたいこと
DB に対しての読み込み・書き込みを伴うバッチ処理を作りたかったです。バッチでは既存の BE と同様、ORM には GORM、モデルの生成には gen を使いたいなと思いました。
モデル、テスト DB のセットアップ処理など、背景
で紹介した GraphQL サーバと一言一句同じコードが生まれてしまうなとは思いつつ、プロジェクトのルートから別でディレクトリを切ってみました。
.
├── batch-job # 追加。バッチ処理のソースコード
│ ├── models # DB モデル。gen で生成
│ │ ├── templates
│ │ └── gen
│ │ ├── hoge_table1.go
│ │ ├── hoge_table2.go
│ │ └── hoge_table3.go
│ ├── services
│ # 途中省略
│ ├── go.mod
│ ├── go.sum
│ └── main.go
├── external # Hasura の Remote Schemas になっている GraphQL サーバ
│ ├── models # DB モデル。gen で生成
│ │ ├── templates
│ │ └── gen
│ │ ├── hoge_table1.go
│ │ ├── hoge_table2.go
│ │ └── hoge_table3.go
│ ├── graph
│ # 途中省略
│ ├── go.mod
│ ├── go.sum
│ └── main.go
├── hasura # Hasura の構成ファイル。Go のプロジェクトではない
batch-job
というディレクトリを切り、そこに新たな module をつくりました。既存の external
とは完全に独立した module としています。
ご覧の通り external
、batch-job
ともに models ディレクトリが切られ、同じファイルが入っています。コードの多重管理が生まれてしまっています。external
の models も batch-job
の models も変更頻度・変更理由が全く同じなので、それぞれ別 module で管理することにメリットはありません。どうにかして、この models を GraphQL サーバからも、バッチ処理からも呼べるように共通化したいです。
Go のマルチモジュール
コードの共通化するために、Go のマルチモジュール機能を使うことにしました。共通化したいコードを shared
という module に切り出しました。DB のモデルの他に、テストの共通処理もついでに切り出しました。
.
├── docker-compose.yml
├── batch-job # バッチ処理のソースコード
│ ├── services
│ # 途中省略
│ ├── go.mod
│ ├── go.sum
│ └── main.go
├── external # Hasura の Remote Schemas になっている GraphQL サーバ
│ ├── graph
│ # 途中省略
│ ├── go.mod
│ ├── go.sum
│ └── main.go
├── shared # 追加。external/batch-job 両方から呼ばれる処理をここへ
│ ├── models # DB モデル。gen で生成
│ │ ├── templates
│ │ └── gen
│ │ ├── hoge_table1.go
│ │ ├── hoge_table2.go
│ │ └── hoge_table3.go
│ ├── go.mod
│ ├── go.sum
│ └── test # テスト用の汎用処理 (DBセットアップ・factoryなど)
├── hasura # Hasura の構成ファイル。Go のプロジェクトではない
マルチモジュールを使うのにいくつかアプローチがあったのですが、今回は replace
ディレクティブを使うやり方にしました。
Replaces the content of a module at a specific version (or all versions) with another module version or with a local directory. Go tools will use the replacement path when resolving the dependency.
特定のバージョン (またはすべてのバージョン) のモジュールの内容を、別のモジュールのバージョンまたはローカルディレクトリに置き換えます。Go ツールは、依存関係を解決するときに置換されたパスを使用します。
replace
は go.mod に指定して使います。 =>
の後ろで指定されたパスに存在しているモジュールを、replace
のあとに書いたパス名で import できます。
サンプルコード
module github.com/example/huga
go 1.15
// module-path: そのモジュール内で使いたいモジュールのパス名。任意のパス名をつけることができる
// actual-path: 実際にそのモジュールが存在する (go.mod が存在する) パス
// replace "module-path" => "actual-path"
replace github.com/example/hoge => ../hoge
package main
import "github.com/example/hoge"
func main() {
hoge.GetHoge()
}
今回やりたいのは、shared という module を batch-job と external の両方から呼べるようにすることでした。replace
を使って、batch-job、external の go.mod で shared のルートパスを任意のパスに置換します。
module github.com/kmtym1998/server-external
go 1.15
replace github.com/kmtym1998/server-shared => ../shared
module github.com/kmtym1998/server-batch-job
go 1.18
replace github.com/kmtym1998/server-shared => ../shared
これでそれぞれの module から shared が使えるようになりました。shared module のパッケージを使う場合は、replace
のあとに書いたパス名を用いて import します。
package hoge
import (
"github.com/kmtym1998/server-shared/models"
"gorm.io/gorm"
)
func insertHoge(db *gorm.DB, param *models.Hoge) error {
return db.Create(param).Error
}
これで shared にある DB モデルが参照できるようになりました 🎉🎉🎉🎉
補足
workspace モードを使わなかったのはなぜ?
go1.18 からは workspace モードを使うことができます。workspace モードを使うともう少しスマートな感じでマルチモジュールの管理ができます。記事が長くなるので詳細は割愛します。
今回 workspace モードを使わなかったのは、external をビルドする docker イメージの環境で go1.15 を使っていたためです。Go は基本的に下方互換を保ったままバージョンが上がっています。external で使う Go のバージョンを 1.18 に上げることを検討してよかったかもしれません。
shared を別リポジトリで管理して、ライブラリ化すればよかったのでは?
shared に含まれる DB モデルは、DB スキーマの変更に追随させる必要があるため、モデルの変更タイミングはスキーマの更新に依存しています。今回取り上げたプロジェクトではマイグレーション管理をしているのが Hasura なので、そこと切り離すのが難しく、同一リポジトリで管理するやり方をとりました。
Discussion