Gormとの破局、そしてFacebook/entとの出会い

# golangとの出会い

御岳山のミツバツツジが紫の花をつけはじめ高山でも徐々にコケモモやアヤメなどのいろとりどりの花が見て取れるようになった時期、RESTよりパフォーマンスの良いgRPCをチャレンジしようという流れでgRPC使うならサーバサイドは折角だしgolang使おうとなった辺りがgolangとの出会い。
散々他所でも言われていますが**golangくんは付き合ってみるとシンプルを売りにしている反面結構頑固な所が多く割と融通の効かない子です。**それ故にアプリケーション全体のアーキテクチャ設計で頭使う必要があり無茶な実装はlinterで弾きやすく**レビュアーにとっては優しくたまに不満はあるけれど全体としてみれば嫌いじゃないかな**という気持ちでここまで付き合いが続いています。
golangくん個人開発ではそこまで旨味なさそうだけど複数人の開発では使えば使うほど味が出てくるスルメみたいな子。golangはいいぞ。
なお本記事は本質部分ではない部分のerrorチェック処理等は全部カットしてあります。ご了承ください。

# Gormとの出会い

そんな**頑固一徹で素材の味を活かすのが持ち味なgolangくん**なのですが、流石にデータベースを扱うときには実装者にconnectionをあんまり意識させたくないし、**データベースから取得したレコードをstructにMappingするために結果セットをfor statementで回すのは面倒くさい上にあまりシンプルな実装にならないだろうなと思い、Active RecordのようなORMを採用しよう**と最初から決めていました。
当初は自分もそこまでgolang界隈に詳しくなかったのでORM libraryの選定の際**ひとまずGithubのスターの数多いし開発止まってなさそうなのでGormを使ってみるか**みたいな感じでGormを選んだのがGormくんとの出会いです。

# Gormと過ごした日々

Gormを導入した時、標準のsqlパッケージのようにQueryを投げて**rows.Next()で回すような手続き型**ではなく

```go
type Group struct {
	ID    int
	Name  string
	Users []User
}

type User struct {
	ID      int
	GroupID int
	Name    string
}

func main() {
	db := connection()
	defer db.Close()

	query := `
SELECT users.id, users.name
FROM groups
INNER JOIN users ON groups.id = users.group_id
WHERE groups.name = ?
`
	groupName := "LiGHTs"
	rows, _ := db.Query(query, groupName)

	var users []*User
	for rows.Next() {
		var user User
		if err := rows.Scan(&user.ID, &user.Name); err != nil {
			log.Fatal(err)
		}
		users = append(users, &user)
	}
	fmt.Println(users)
}

func connection() *sql.DB {
	// 省略
}
```

**メソッドチェーンで記述できQueryの構築からDBへの発行structへのMappingも行ってくれるため**「おっ結構便利なやつだな」というのが最初の印象。

```go
type Group struct {
	ID    int
	Name  string
	Users []*User
}

type User struct {
	ID      int
	GroupID int
	Name    string
}

func main() {
	db := connection()

	var group Group
	if err := db.Preload("Users").
		Where(Group{Name: "LiGHTs"}).
		Find(&group).Error; err != nil {
		fmt.Println(err.Error())
		return
	}
	fmt.Println(group)
}

func connection() *gorm.DB {
	// 省略
}
```

その後は記事を更新しつつGormと仲良くなるべく日々を過ごしていました。

[gormと仲良くなりたい(1) - gorm の Where/Update で struct を使いたい](https://qiita.com/tomtwinkle/items/ab46f7972b2b472c6ae7)
[gormと仲良くなりたい(2) - datetimeでマイクロ秒(6桁)まで格納/取得する](https://qiita.com/tomtwinkle/items/7aa4b28bab3dd0a5fd6f)

# Gormとの破局

[GormのDELETEで誤爆しないために](https://qiita.com/tomtwinkle/items/48e2fa94ff0624b717b7)
のような記事を書いていた頃、実装の際に壁にぶち当たりドキュメントを調べてGorm本体のソースを追ってるうちに**「あれ？もしかしてGormのイケてないのでは？」**と徐々に思うようになってきてしまいました。
そして、イケてないと思い始めると思い当たる節がいくつも出てきてパッと思いつく限り挙げていくと

* **idの値が指定されていないと普通に[全件Delete*1](#1-全件delete)しに行ったり**
* **structの値に[ゼロ値*2](#2-ゼロ値)設定されていたら更新その項目更新しなかったり**
* 同じくstructでwhere指定していると **`SELECT * FROM hoge WHERE type = 1` は動くが `SELECT * FROM hoge WHERE type = 0` は正常に動作しなかったり**
* 非transactionとtransactionのオブジェクトが同じなのでコード上transaction処理なのかどうか判断しにくかったり
* **[v1.gorm](https://v1.gorm.io/)と[gorm(v2)](https://gorm.io/)が存在し、[双方に互換性がない*3](#3-双方に互換性がない)**上にどちらのドキュメント見ているのか分かりにくかったり
* [schema structは単数形で定義しているのに参照する物理tableは勝手に複数形になったり](https://gorm.io/docs/conventions.html#Pluralized-Table-Name)

主に **コーディング時に気付かずbug埋め込んでしまうタイプの罠が多い事** と **ドキュメントが割と不親切で読んでも解決できずに結局gorm本体のコードを読む**事が多い所が物凄い不満が溜まっていき敢え無く破局へと追い込まれていきました。

破局の根本の原因としては

- Gormは「コードの記述量を減らす事」を最優先として設計されており**「コードに書かれていない事も"意図せず"ある程度いい感じに動いてしまう」**ようになっていること
- Select結果のMapping先だけでなくWhere条件やUpdate対象までinterfaceで引数を受け取り**reflectionで頑張って解析するという作りにしている為に、Gormの内部でstruct内のfieldのゼロ値がゼロ値なのかnilなのか判断できない**という問題にぶち当たり、結果的に**Gormに値がゼロ値のstructを渡した場合初めて分かるタイプのbugが量産**されてしまったこと
- TABLE JOINやPreloadの仕様が独特で内部でreflectionを行っている都合「field名(=物理column名)」を意識しなくてはいけないこと。あとそれに伴い**Preload時にはstringでfield名を定義する**必要があること

ここらへんなのかなと思っています。
unit testでの値のケースでたまに漏れているとdeploy後にbugに気づく事が度々あり実装者が常にそこを意識して実装するのは結構ストレスが高かったです。

# Facebook/entとの出会い

Gormの反省を踏まえて**[genericsがなくて*4](#4-genericsがない)型にうるさいgolangとしてのlibraryの在り方としてはinteface型で引数を受け取りreflectionを多用して頑張る方向ではなく、codeで定義書いてその定義からcodeをgenerateする方向が向いているのではないか**と思いはじめた矢先[Facebook/Ent](https://github.com/facebook/ent)に出会いました。

Gormからの乗り換え先を探す際の最低限の基準として考えていた

* schema定義をcodeで書けること
* schema定義からQuery生成関連のcodeをgenerate出来ること
* schema定義が正しいかgenerate時にチェックしてくれること
* 静的型付けされたSetter/Getterが用意されていること
* generateされるcodeがある程度カスタマイズ出来ること
* Datadogを使用してOpenTracingの出力が確認できるよう[Go Datadog Trace](https://docs.datadoghq.com/ja/tracing/compatibility_requirements/go/#%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E4%BA%92%E6%8F%9B%E6%80%A7)に対応しているか

の条件を満たし、かつまあ企業が運用しているならGormがやらかしていたデータ全消し仕様なんて妙なトラップもないだろうという打算もありつつ新しく付き合い始めました。

さっきまでの実装をFacebook/entで実装すると

schema定義ここから ---->

```go:schema/groups.go
// Groups holds the schema definition for the Groups entity.
type Groups struct {
	ent.Schema
}

// Fields of the Groups.
func (Groups) Fields() []ent.Field {
	return []ent.Field{
		field.String("name"),
		field.String("leader").Optional(),
	}
}

// Edges of the Groups.
func (Groups) Edges() []ent.Edge {
	return []ent.Edge{
		edge.From("users", Users.Type).
			Ref("group"),
	}
}
```

```go:schema/users.go
// Users holds the schema definition for the Users entity.
type Users struct {
	ent.Schema
}

// Fields of the Users.
func (Users) Fields() []ent.Field {
	return []ent.Field{
		field.String("name"),
	}
}

// Edges of the Users.
func (Users) Edges() []ent.Edge {
	return []ent.Edge{
		edge.To("group", Groups.Type).
			StorageKey(edge.Column("group_id")).
			Unique().
			Required(),
	}
}
```

<---- schema定義ここまで
schema定義はテーブルごとに必要になります。

以下が実装の本体。

```go:main.go
func main() {
	ctx := context.Background()
	client := connection()

	group, err := client.Groups.Query().
		WithUsers().
		Where(groups.Name("LiGHTs")).
		Only(ctx)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(group.Edges.Users)
}

func connection() *ent.Client {
	// 省略
}
```

コード生成するので**コード量自体は10倍くらいに増えますが**、schema定義さえしっかりしていれば
**必要そうなAPIが大体実装されている**ので、例えばGormで散々悩まされたゼロ値とnilの区別の問題もschema側でfieldをOptionalで定義すれば

```go:schema/groups.go
// Fields of the Groups.
func (Groups) Fields() []ent.Field {
	return []ent.Field{
		field.String("name"),
		field.String("leader").Optional(),
	}
}
```

```go:main.go
	group, err = client.Groups.Query().
		WithUsers().
		Where(groups.LeaderIsNil()).
		Only(ctx)
```

のようにIS NULL検索なども問題なく行えるようになります。
※念のためGormでIS NULL検索が出来ないわけではないです。あくまでWhereにstructを渡した場合の話。

# Facebook/entを使ってみての所管

Facebook/entを使い始めた早1か月ちょい、まだ使い始めたばかりでいくらか検証の余地は残ってはいるのですがFacebook/entの特徴と実装してみたところの感想としては以下の通りです。

- **まだv1はリリースされていない絶賛開発中**
- Go templatesを利用したQuery生成APIのgenerateがメイン
- Go templatesを記載する事により一部生成codeの拡張が可能
- ドキュメントが比較的丁寧（だが、Edgeの説明はあんまり親切じゃない）
- 実装時の制約となるvalidation checkの仕組みが整っている
- **generate時のerrorメッセージが分かりやすい** ←割とありがたい
- **schemaからgenerateされたQuery生成APIは静的型付けが行われる**
  - ただしselectで取得するcolumn絞ったりするとreflectionが使用されたりはする
- **TABLE JOIN statementは書けない**
    - [Graph Traversal](https://entgo.io/docs/traversals/)という独特の思想(結合対象のテーブルに対して対象のid一覧をin句に指定したselectを自動で行う)
- 複合PKに対応出来ないわけではないがあまり考慮はされていない
- **PKの名前がidじゃない場合schemaの記述にもひと手間工夫が必要**
- **PKの名前がidじゃない場合のO2O周りの結合の挙動が怪しい**
- **Upsertには現状対応していない** [協議はされている](https://github.com/facebook/ent/issues/139)
- **`SELECT FOR UPDATE`のようなqueryが書けない** [協議はされている](https://github.com/facebook/ent/issues/358)

物理schemaがAPI毎の概念schemaに合わせて設計されているならば恐らく問題なく使用できる子であるとは思いますが、**頑張ってJOINしないとデータが取れないテーブル設計やパフォーマンス重視のマジカルなテーブル設計の場合は厳しそうです。**ここら辺はRawで生SQLが書ける分まだGormの方が柔軟性がありました。

幸いにも今回は元々APIに合わせて物理schemaが設計されており、Gormからの乗り換えで気になったのはGorm側の仕様でテーブル名が複数形になってる事とO2Oでの結合くらいでした。
後者は**PKの名前と型が`id int`ではない場合、[定義は出来ます](https://entgo.io/docs/schema-fields/#id-field)がO2OのEdgeを定義するとGraph Traversalでrecordが取得できなかったり**、そこだけはちょっと予想外な動作をしていたためもしかしたらバグなのかもしれません。Facebook/entの実装はgoではなくgo templateなので読むのがちと辛いですがバグの内容が分かったらPRを出そうかと。
どちらにせよ現在開発中なのでこの問題は時間経過で解決されるとは思います。

Graph Traversal周り分かりにくい所はありますが現状問題なく使えてはいます。
とはいえUpsertは実装でどうにかなりますが`SELECT FOR UPDATE`辺りは早めに欲しいかな。

**クリスマスが終わったらまた破局しないようにお祈りしつつ**　今回はここまで。


# 補足
#### *1 全件DELETE
- [Gormが本番テーブルの数億件のデータを消そうとした話](https://scrapbox.io/keroxp/Gorm%E3%81%8C%E6%9C%AC%E7%95%AA%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%81%AE%E6%95%B0%E5%84%84%E4%BB%B6%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E6%B6%88%E3%81%9D%E3%81%86%E3%81%A8%E3%81%97%E3%81%9F%E8%A9%B1)
- [GormのDELETEで誤爆しないために](https://qiita.com/tomtwinkle/items/48e2fa94ff0624b717b7)

ちなみにgorm v1での動作です。v2ではerrorになるので突然消されることはなくなりました。

#### *2 ゼロ値
`string = ""` `int = 0` `bool = false` 等の要するに初期値の事。

GormではUpdateの際に更新値としてstrutcを渡せるが、struct定義がゼロ値の場合は更新されない。[公式ドキュメントにもサラッと記載](https://gorm.io/docs/update.html#Updates-multiple-columns)がある。

```go
type User struct {
	Name   string
	Age    int
	Active bool
}

// Update attributes with `struct`, will only update non-zero fields
db.Model(&user).Updates(User{Name: "", Age: 0, Active: false})
```
上記の値でUpdateを行った場合は**何も更新されない**。

#### *3 双方に互換性がない
例えば[gorm v1ではPrimaryKeyのstruct tagの指定が`primary_key`](https://v1.gorm.io/docs/composite_primary_key.html)だが[gorm v2では`primaryKey`である](https://gorm.io/docs/composite_primary_key.html)。
Gormは2つのバージョンがありドキュメントは分かれているという前提条件を知らずにドキュメントを見ていたりすると想定通りに動かなくてハマる。何故ドキュメントを2つに分けたし。

#### *4 genericsがない
golang 次期メジャーバージョン辺りでは正式に実装されるはずなので大幅に便利になりそう。
[The Next Step for Generics](https://blog.golang.org/generics-next-step)