Go製のORM である Facebook / ent を使ってみたらなかなかイケてた話

# はじめに
Facebookが開発しているGo製のORMであるentを業務で使い始めたので、基本的な構文などをまとめます。

# 導入
-  CRUD操作についての確認
https://entgo.io/docs/crud

- validatorの定義方法の確認
https://entgo.io/docs/schema-fields/#validators

- migrationの実行方法及びversioned migrationの調査
https://entgo.io/docs/migrate#auto-migration
https://entgo.io/docs/versioned-migrations#generating-versioned-migration-files
https://entgo.io/docs/versioned-migrations#apply-migrations

- association(entだとedge)の定義方法の確認
https://entgo.io/docs/schema-edges/


# datetimeを更新しようとした際に発生したエラー

```
failed scanning rows: sql: Scan error on column index 8, name "updated_on": unsupported Scan, storing driver.Value type []uint8 into type *time.Time
```

```go
	client, err := ent.Open("mysql", "root:pass@tcp(localhost:3307)/test?parseTime=true")
```


https://budougumi0617.github.io/2019/03/31/go_db_unsupported_scan_storing_driver_value_type_uint8_into_type_time_time/

# リレーションを定義した別テーブルのカラムを取得する方法

まず、 Edgesの部分を以下の様に定義して、
```go:ent/schema/archive_file.go

func (ArchiveFile) Edges() []ent.Edge {
	return []ent.Edge{
		edge.From("organization", OperatorCoMaster.Type).
		Ref("archiveFiles").
		Unique().
		Required().
		Field("operator_co_id"),
	}
}
```

```go:ent/schema/operator_co_master.go

func (OperatorCoMaster) Edges() []ent.Edge {
	return []ent.Edge{
		edge.To("archiveFiles", ArchiveFile.Type),
	}
}
```

archivefileの全レコードを取得した後に、for文でoperatorCoMasterを一つずつ取り出せばいけた。

```go
func toProtoArchive(af *ent.ArchiveFile, ocm *ent.OperatorCoMaster) *genproto.Archive {
	v := &genproto.Archive{
		Id: int32(af.ID),
		FileName: af.ArchiveFileName,
		FileTitle: af.ArchiveFileTitle,
		OperatorCoName: ocm.OperatorCoName,
	}
	return v
}

func (svc *ArchiveService) List(ctx context.Context, req *genproto.ListArchiveRequest) (*genproto.ListArchiveResponse, error) {
	archives, err := svc.client.ArchiveFile.Query().All(ctx)

	if err != nil {
		return nil, status.Errorf(codes.Internal, "internal: %s", err)
	}

	var keys []*genproto.Archive
	for _, v := range archives {
		operatorcomaster, _ := v.QueryOrganization().Only(ctx)
		po := toProtoArchive(v, operatorcomaster)

		keys = append(keys, po)
	}

	return &genproto.ListArchiveResponse{ArchiveList: keys}, nil
}
```

https://tech.smartcamp.co.jp/entry/try-go-ent

## feature flug

```terminal
go run entgo.io/ent/cmd/ent generate --feature sql/upsert ./ent/schema
```

# migration
entのschemaファイルから生成されたマイグレーションファイルをDBに適用するためには `golang-migrate`を使用する必要があります。

## apply migration
`./migrations` 以下に用意されたファイル内のクエリが実行されます。

* upファイルのSQLがDBに実行される。
```terminal
migrate -source file://migrations -database 'mysql://root:pass@tcp(localhost:3307)/test' up
```

* downファイルのSQLがDBに実行される。

```terminal
migrate -source file://migrations -database 'mysql://root:pass@tcp(localhost:3307)/test' down
```

## force 
特定のバージョンにまで戻す場合

```
migrate -source file://migrations -database 'mysql://root:pass@tcp(localhost:3307)/test' force {$VERSION}
```

https://github.com/golang-migrate/migrate

https://asapoon.com/golang/4002/golang-migrate/


# db pull
既存DBにて既に定義ずみのテーブルを引っ張ってきたい場合には`entimport`というパッケージを使用する必要があります。

https://entgo.io/ja/blog/2021/10/11/generating-ent-schemas-from-existing-sql-databases/

## パッケージのインストール

```terminal
go get ariga.io/entimport/cmd/entimport   
```

## 既存テーブルを持ってくる

```terminal
go run ariga.io/entimport/cmd/entimport -dsn "mysql://root:pass@tcp(localhost:3307)/test" -tables client_account_master
```

# fields
## MySQLのカラムにマッピングする
ent側で用意されている型定義とMySQL側の型定義が若干違っている場合、以下の様にMySQL側の型定義に対してマッピングしてあげることができます。

```go
	field.String("condition1").Optional().SchemaType(map[string]string{
			dialect.MySQL: "varchar(2)",
		}),
```

https://entgo.io/docs/schema-fields/#database-type

# index
# uniqeuキー制約をindex名称を明示した上で定義する

```go
func (User) Indexes() []ent.Index {
    return []ent.Index{
		index.Fields("asp_id", "relation_unique_id", "client_site_id", "start_datetime").
			StorageKey("UNIQUE").
			Unique(),
    }
}
```

https://entgo.io/docs/schema-indexes#multiple-fields
https://entgo.io/docs/schema-indexes#storage-key

# annotation
## 文字コードを明示する

```go:ent/schema/user.go
func (User) Annotations() []schema.Annotation {
	return []schema.Annotation{
		entsql.Annotation{
			Collation: "utf8_general_ci",
			Charset: "utf8",
		},
	}
}
```
https://entgo.io/docs/faq/#how-to-change-the-character-set-andor-collation-of-a-mysql-table

## テーブル名の指定
デフォルトではテーブル名が複数形になってしまうので、既にテーブルが作成済みでテーブル名を既存のもので指定したい場合には、以下の様にしてテーブル名を明示する必要があります。

```go:ent/schema/user.go
func (User) Annotations() []schema.Annotation {
	return []schema.Annotation{
		entsql.Annotation{
			Table: "user",
		},
	}
}
```

https://entgo.io/docs/schema-annotations#custom-table-name

# 残念なところ
entでは複合primary keyを定義することができません。

# entのschemaからER図を作成する
entのschemaファイルからER図を生成するためのツールが公開されています。

https://github.com/hedwigz/entviz

## パッケージのインストール

```terminal
go get github.com/hedwigz/entviz/cmd/entviz
```

# ER図を生成する

```terminal
go run github.com/hedwigz/entviz/cmd/entviz ./ent/schema
```
上記コマンドを実行すると以下のようなER図が自動生成されます。
シンプルなER図になっていてエンティティ間のリレーションが直感的にわかりやすいですね。

https://entgo.io/ja/blog/2021/08/26/visualizing-your-data-graph-using-entviz/![](https://storage.googleapis.com/zenn-user-upload/9e3a5d3e5ac1-20220517.png)

# 終わりに
公式ドキュメントに最低限の使用方法しか書かれておらず、開発を始めて間もない頃には、「なかなか開発速度が出ないなぁ」、という印象を持っていましたが、慣れてくるとサクサクと開発が進み、非常に良くできたフレームワークであることを実感しました。
今後開発を進めていく中で、他のユースケースも追記していきたいと思います。