この記事はWHITEPLUS Advent Calendar 2018 - Qiita 18日目になります。
こんにちは、WHITEPLUSのサーバーサイドエンジニア八巻です。 マネージャーをしながら、リネットの設計リードをしたりしています。
この記事の背景
リネットの環境ではGoのORMとしてGormを利用しています。
Goの環境ではレイヤードアーキテクチャに基いてDBへのアクセスをRepositoryパターンを実装することが多いのですが、ここでのGormの使い方が実装者によってバラバラなので、Repositoryパターンとしてはこれが良さそうというのをまとめてみました。
基本的に公式のドキュメントに書いてある内容になってます。
(ドキュメントの中から適切な箇所探す出すの大変ですよね。。)
Repositoryパターン
ここで指しているリポジトリパターンはDDD(ドメイン駆動設計)の文脈で出てくるものを想定しています。
Repositoryの目的は大きく2つあります。
- 集約(もしくはEntity)をDBへ「保存(save)」する
- DBから集約(もしくはEntity)を「呼び出す(of)」
保存(save)
ドメインロジックで計算した結果のドメインオブジェクトを「保存」するとこが目的なので、insert or updateを透過的に扱えるようにします。
gormではAssignとFirstOrCreateを利用してこの機能を実装します。
ドキュメントは見つけづらいですが、Queryの中のAssignに記述があります。
http://doc.gorm.io/crud.html#query
// DTO
type User struct {
ID int `gorm:"primary_key"`
Name string
Age int
}
// saveはgormのFirstOrCreateを使う
func (r *Repository) Save(u domain.User) error {
return r.DB.Where(User{ID: u.ID.Int()}).
Assign(User{
Name: u.Name.String(),
Age: u.Age.Int()}).
FirstOrCreate(&User{}).Error
}
この例ではUserIDが存在すれば更新(update)、存在しなければ登録(insert)されることになります。
呼び出し(of)
呼び出しに関しては、単純なものであればQueryで十分なのですが、いくつもテーブルをつなげてモデルを構築する際にはEager Loadingをしないと直ぐにN+1問題を発生させてしまします。
GormのドキュメントではPreLoadingとして紹介されてます。
http://doc.gorm.io/crud.html#preloading-eager-loading
type Order struct {
ID int `gorm:"primary_key"`
Occurred time.Time
Price int
User User
UserID int
}
type User struct {
ID int `gorm:"primary_key"`
Name string
Age int
}
func (r *Repository) Of(id id.OrderID) (out Order, err error) {
err = r.DB.
Where("id = ?", id.Int()).
Preload("User").
Find(&out).Error
return
}
Orderの中のUserをこうすることで一緒にとってくることが出来ます。
今回の例ではないですが、Orderを複数とってくる場合は必要なuserをinでとって来てくれるので、N+1問題を防ぐことが出来ます。
まとめ
今回はGormの中でもRepositoryパターンにおいて使うと便利な機能を取り上げてみました。
Gormはシンプルながらも一通りのORMとしての機能が揃っていて、ドキュメントも充実してますので一度見直してみると新たな発見があって面白いです。
明日は来年の新卒?エンジニア@maaaasの「オブジェクト指向もわからない未経験がベンチャーIT企業に入社して2ヶ月でやったコト4つ」になります。