こんにちは。スマートキャンプ エンジニアの中田です。 皆さんはGoのORMには何を使われていますか? 有名どころだと機能の豊富な GORM や取得データのマッピング部分だけを担うシンプルな sqlx 、 最近だとテーブル定義からモデルコードの自動生成してくれる SQLBoiler など、Goには多くのORMがあります。 筆者のORM遍歴は以下のようになってます。 Active Record(Ruby on Rails): 2年ほど GORM(Go): 半年ほど 弊社のプロダクトのバックエンドは Ruby on Rails で作られているものがほとんどです。 Ruby on Rails を利用しての開発経験が私のキャリアの大半を占めていることもあり、個人的に ActiveRecord のような機能の網羅率の高いORMには安心感を覚えます。 半年前から新規で開発を始めたプロダクトにて、新たに Go を利用し始めました。 弊社のGo製プロダクトのORMには GORM を利用しています。 GORMはドキュメントも充実しており機能自体も豊富であるため、特に事なく利用できています。 しかし、validatorが組み込みでない点や、Auto migrationで up は可能ですが down はできない点など若干の物足りなさも感じています。 そこで、本記事では GORM に取って代わる新たなORMを探るべく、 Facebook Connectivity チームより開発された、 ent というORMを調査してみます。 「ent」とは 特徴 グラフ構造 触ってみた スキーマの定義 ent/schema/user.go ent/schema/company.go ent/schema/time_mixin.go Mixin Fields Edges ent/schema/user.go ent/schema/company.go コード生成 CRUD APIを作成してみる DB接続 READ UPDATE DELETE CREATE(Transaction) 良かった点・もうひとつだった点 良かった点 自動生成機能の強力さ ドキュメントの充実度 機能の豊富さ もうひとつだった点 Schemaの管理 まとめ 「ent」とは 前述したように、 ent は Facebook Connectivity チームにより開発されているGoのORMです。 GitHubリポジトリからRelease履歴を辿ってみると v0.1.0 が2020年1月に公開されており、GoのORMの中では比較的新しい方に分類されるのではないでしょうか。 特徴 ent の特徴を 公式 より引用すると以下です。 シンプルながらもパワフルなGoのエンティティフレームワークであり、大規模なデータモデルを持つアプリケーションを容易に構築・保守できるようにします。 ・Schema As Code(コードとしてのスキーマ) - あらゆるデータベーススキーマをGoオブジェクトとしてモデル化します。 ・任意のグラフを簡単にトラバースできます - クエリや集約の実行、任意のグラフ構造の走査を容易に実行できます。 ・100%静的に型付けされた明示的なAPI - コード生成により、100%静的に型付けされた曖昧さのないAPIを提供します。 ・マルチストレージドライバ - MySQL、PostgreSQL、SQLite、Gremlinをサポートしています。 ・拡張性 - Goテンプレートを使用して簡単に拡張、カスタマイズできます。 ・Schema As Code(コードとしてのスキーマ) - あらゆるデータベーススキーマをGoオブジェクトとしてモデル化します。 ・任意のグラフを簡単にトラバースできます - クエリや集約の実行、任意のグラフ構造の走査を容易に実行できます。 ent ではスキーマファイルの定義からGoのGeneratorを利用してモデル、DBスキーマを自動で生成してくれます。自動生成したモデルには定義内容を元にクエリビルド用の汎用関数も作成され、DBへの処理実行時にはそのクエリビルド用の関数をチェーンして実現したいクエリを組み立てていきます。 ・100%静的に型付けされた明示的なAPI - コード生成により、100%静的に型付けされた曖昧さのないAPIを提供します。 Goには v1.17.X 時点でジェネリクスが入っていないこともあり、GORMなど他のORMでは interface{} を利用した抽象化でオープンに引数を受け取り、内部で型を判別するような実装が多いと思います。 ent ではスキーマ定義からモデルやフィールドごとにコードを自動生成するため、それぞれの型に合った関数を利用でき100%の静的な型付けが実現されています。 また、 ent も GORM 同様にドキュメントが充実しています。 日本語翻訳もされており、本記事の執筆にあたり ent を実際に利用してみた際に生じた困りごとはほぼほぼ 公式ドキュメント を参照すれば解決できました。 グラフ構造 ent ではグラフ構造に基づいてスキーマを定義していきます。 グラフ構造とは以下の図のようにノード(節点・頂点、点)の集合とエッジ(枝・辺、線)の集合で構成される構造のことです。このように構造化することでさまざまなオブジェクトの関連を表すことができます。 この図では参照元、参照先を表現しない無向グラフが書かれていますが、 ent の場合は紐づきを矢印で表す有向グラフで構造化されます。 グラフ(WikiPediaより) WikiPedia - グラフ理論 ent におけるノードはモデル、エッジはモデルのリレーションを指します。 初期化時点でのスキーマには一つのノードに対して、 Fields 、 Edges メソッドが生えた状態でコードが生成されます。細かな定義方法は後述しますが、ここで Fields にはモデルのフィールドを、 Edges には、モデルのリレーションを定義します。 触ってみた それでは実際に ent を触ってみます。 実装環境は以下です。 OS: macOS BigSur Go: v1.17.1 MySQL: v5.7.35 -- サンプルアプリで使用しているライブラリ -- ORM: (ent)[https://entgo.io/ent] ルーター: (chi)[https://github.com/go-chi/chi] Null値: (null)[https://github.com/guregu/null] サンプルアプリのソースコードは(ココ) https://github.com/kiki-ki/lesson-ent から参照可能です。 初めに作業用にワークスペースを切り、 ent のCLIをインストールします。 go install entgo.io/ent/cmd/ent@latest スキーマの定義 今回は以下のデータ構造で作成していきます。 companies : users = 1 : N companies --- id: bigint auto increment pk name: varchar(255) not null created_at: timestamp updated_at: timestamp --- users --- id bigint auto increment pk company_id: bigint not null name: varchar(255) not null email: varchar(255) not null unique role: enum('admin', 'normal') comment: varchar(255) nullable created_at: timestamp updated_at: timestamp --- 以下のコマンドを実行して、 User 、 Company スキーマファイルを自動生成します。 ent init User Company ent/schema ディレクトリ配下に各モデルのスキーマファイルが生成されました。 このファイルを編集していきます。 上述のデータ構造を再現するために、以下のようにスキーマを定義しました。 ent/schema/user.go package schema ...snip // User holds the schema definition for the User entity. type User struct { ent.Schema } // Mixin of the User. func (User) Mixin() []ent.Mixin { return []ent.Mixin{ TimeMixin{}, } } // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.Int( "company_id" ), field.String( "name" ), field.String( "email" ).Unique(), field.Enum( "role" ).Values( "admin" , "normal" ), field.Text( "comment" ). Optional(). Nillable(). GoType(null.String{}), } } // Edges of the User. func (User) Edges() []ent.Edge { return []ent.Edge{ edge.From( "company" , Company.Type). Ref( "users" ). Unique(). Required(). Field( "company_id" ), } } ent/schema/company.go package schema ...snip // Company holds the schema definition for the Company entity. type Company struct { ent.Schema } // Mixin of the Company. func (Company) Mixin() []ent.Mixin { return []ent.Mixin{ TimeMixin{}, } } // Fields of the Company. func (Company) Fields() []ent.Field { return []ent.Field{ field.String( "name" ), } } // Edges of the Company. func (Company) Edges() []ent.Edge { return []ent.Edge{ edge.To( "users" , User.Type). Annotations(entsql.Annotation{ OnDelete: entsql.Cascade, }), } } ent/schema/time_mixin.go package schema ...snip type TimeMixin struct { mixin.Schema } func (TimeMixin) Fields() []ent.Field { return []ent.Field{ field.Time( "created_at" ).Immutable().Default(time.Now), field.Time( "updated_at" ).Default(time.Now).UpdateDefault(time.Now), } } コードの説明をしていきます。 Mixin 最初に Mixin メソッドに注目してみます。 ent では汎用性の高いフィールド群を Mixin として切り出して別スキーマに注入できます。サンプルコードでは time_mixin.go に created_at 、 updated_at の2フィールドをセットで切り出し、 company 、 user の両スキーマにMixinしています。同ペアのMixinはライブラリのデフォルトでも mixin.Time として組み込まれていますが、今回はカスタムMixinで新たに定義してみました。 Fields 次に Fields メソッドに注目してみます。ここにはメソッド名の通りにモデルのフィールドを定義します。 func (User) Fields() []ent.Field { return []ent.Field{ field.Int( "company_id" ), field.String( "name" ). Validate(validation.BlackListString([] string { "hoge" , "fuga" })),, field.String( "email" ).Unique(). Match(regexp.MustCompile(validation.EmailRegex)), field.Enum( "role" ).Values( "admin" , "normal" ), field.Text( "comment" ). Optional(). SchemaType( map [ string ] string { dialect.MySQL: "text" , }). GoType(null.String{}), } } 基本的にはフィールドごとに ent 組み込みの型から任意の型を指定しそのメソッドにテーブルのカラム名を渡せば、モデルのフィールドとテーブルのカラム定義は完了です。 id フィールドはデフォルトで作成されるため記載不要です(同名のフィールドを定義すれば設定の上書きも可能)。 あとは定義した各フィールドにメソッドチェーンする形で細かい定義をしていきます。 Unique: ユニーク制約をかける Values: Enum値を設定する Optional: モデルのCreate時などにこのフィールドを任意の項目にする(デフォルトは必須) SchemaType: データベースのカラム型を独自にマッピングする(Textメソッドのデフォルトは longtext ) GoType: モデルのフィールド型を独自にマッピングする(ここではnull値を許可できる型を指定) Validate: バリデーションを適用する Validate メソッドの引数にはフィールドの型を引数に error を返す関数をアサインします。以下に使用例を示します。 また、 ent 組み込みのバリデーションも多くあり、上記のコードで利用している Must もその内の一つです。定義されたバリデーションはモデルの Save メソッドをコールしたタイミングでフックされます。 package validation ...snip func BlackListString(blackList [] string ) func (s string ) error { return func (s string ) error { isBlackList := false for _, u := range blackList { if s == u { isBlackList = true break } } if isBlackList { return fmt.Errorf( "%sは許可されない文字列です" , s) } return nil } } ent ではサンプルアプリで利用しているものの他にも多くのフィールドのオプションメソッドが用意されています。 詳しくは 公式ドキュメント をご参照ください。 Edges 最後に Edges メソッドです。冒頭でも少し触れたように ent におけるエッジはモデル間のリレーションを指します。サンプルアプリではCompany-User間でOne to Manyなリレーションを定義します。 ent/schema/user.go ...snip // Edges of the User. func (User) Edges() []ent.Edge { return []ent.Edge{ edge.From( "company" , Company.Type). Ref( "users" ). Unique(). Required(). Field( "company_id" ), } } ent/schema/company.go ...snip // Edges of the Company. func (Company) Edges() []ent.Edge { return []ent.Edge{ edge.To( "users" , User.Type). Annotations(entsql.Annotation{ OnDelete: entsql.Cascade, }), } } 上記のようにリレーションを定義できました。この辺りはかなりライブラリ固有な書きっぷりになっている印象です。 Fields 同様に Edges にもオプションメソッドが用意されており、それらを使って細かな設定が可能です。One to Manyの他にもOne to OneやMany to Many、自己ループなどのリレーションにも対応しています。 詳細は 公式ドキュメント をご参照ください。 ※1つ疑問だったのが、Userに定義している外部キー company_id を Required メソッドで not null なフィールドとして定義したのですが上手くいきませんでした。公式ドキュメントと同様の記述をしたつもりだったのですが...。こちら有識者の方いらっしゃればSNS, はてブコメントなどでご教授いただけると助かります。 コード生成 前置きが長くなりましたが、定義したスキーマの情報を元に以下のコマンドでコードを生成します。 go generate ./ent 実行すると ent/ 以下に大量のコードが生成されます。 CRUD APIを作成してみる DB接続 まずはDBに接続します。 ent には組み込みでAuto migration機能があるのでそちらも利用してみます。 package main ...snip func main() { entClient := database.NewEntClient() defer entClient.Close() entClient.Migrate() ...snip } ...snip --- package database ...snip type EntClient struct { *ent.Client } func NewEntClient() *EntClient { dsn := fmt.Sprintf( "%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true" , os.Getenv( "DB_USER" ), os.Getenv( "DB_PASS" ), os.Getenv( "DB_HOST" ), os.Getenv( "DB_PORT" ), os.Getenv( "DB_NAME" ), ) client, err := ent.Open(dialect.MySQL, dsn) if err != nil { panic (fmt.Sprintf( "failed openning connection to mysql: %v" , err)) } env := os.Getenv( "ENV" ) // デバッグモードを利用 if env != "staging" && env != "production" { client = client.Debug() } return &EntClient{client} } func (c *EntClient) Migrate() { err := c.Schema.Create( context.Background(), migrate.WithDropIndex( true ), migrate.WithDropColumn( true ), ) if err != nil { log.Fatalf( "failed creating schema resources: %v" , err) } } go run ./main.go を実行するとMigrate処理が走ります。以下の内容でテーブルが作成されました。 mysql> desc users; +------------+------------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+------------------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | created_at | timestamp | YES | | NULL | | | updated_at | timestamp | YES | | NULL | | | name | varchar(255) | NO | | NULL | | | email | varchar(255) | NO | UNI | NULL | | | role | enum('admin','normal') | NO | | NULL | | | comment | text | YES | | NULL | | | company_id | bigint(20) | YES | MUL | NULL | | +------------+------------------------+------+-----+---------+----------------+ 8 rows in set (0.00 sec) mysql> desc companies; +------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+--------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | created_at | timestamp | YES | | NULL | | | updated_at | timestamp | YES | | NULL | | | name | varchar(255) | NO | | NULL | | +------------+--------------+------+-----+---------+----------------+ 4 rows in set (0.00 sec) 続いてCRUD処理を作成します。controllerの定義は以下のようになってます。 package controller ...snip // *database.EntClientは*ent.Clientをラップした構造体 func NewCompanyController(dbc *database.EntClient) CompanyController { return &companyController{ dbc: dbc, ctx: context.Background(), } } type CompanyController interface { Show(http.ResponseWriter, *http.Request) Update(http.ResponseWriter, *http.Request) Delete(http.ResponseWriter, *http.Request) IndexUsers(http.ResponseWriter, *http.Request) CreateWithUser(http.ResponseWriter, *http.Request) } type companyController struct { dbc *database.EntClient ctx context.Context } READ func (c *companyController) Show(w http.ResponseWriter, r *http.Request) { cId, err := strconv.Atoi(chi.URLParam(r, "companyId" )) // error handling company, err := c.dbc.Company.Get(c.ctx, cId) // error handling w.WriteHeader(http.StatusOK) render.JSON(w, r, company) } 2021/10/20 09:11:15 driver.Query: query=SELECT DISTINCT `companies`.`id`, `companies`.`created_at`, `companies`.`updated_at`, `companies`.`name` FROM `companies` WHERE `companies`.`id` = ? LIMIT 2 args=[1] Showは受け取った id で companies テーブルに検索をかけ、マッチしたレコードを取得するメソッドです。 *ent.Client(dbc) から対象のテーブルを決め( Company )、主キーでの検索用の Get メソッドでレコードを取得します。 func (c *companyController) IndexUsers(w http.ResponseWriter, r *http.Request) { cId, err := strconv.Atoi(chi.URLParam(r, "companyId" )) // error handling company, err := c.dbc.Company.Get(c.ctx, cId) // error handling users, err := company.QueryUsers().All(c.ctx) // error handling w.WriteHeader(http.StatusOK) render.JSON(w, r, users) } 2021/10/20 09:18:40 driver.Query: query=SELECT DISTINCT `companies`.`id`, `companies`.`created_at`, `companies`.`updated_at`, `companies`.`name` FROM `companies` WHERE `companies`.`id` = ? LIMIT 2 args=[1] 2021/10/20 09:18:40 driver.Query: query=SELECT DISTINCT `users`.`id`, `users`.`created_at`, `users`.`updated_at`, `users`.`company_id`, `users`.`name`, `users`.`email`, `users`.`role`, `users`.`comment` FROM `users` WHERE `company_id` = ? args=[1] IndexUsersは、まずShow同様に受け取った id で companies テーブルに検索をかけ、取得した企業に属するユーザーの一覧を返すメソッドです。 まず、 *ent.Client(dbc) から対象のテーブルを決め( Company )、主キーでの検索用の Get メソッドでレコードを企業を取得します。 その後、取得した企業モデルから QueryUsers でスキーマで設定した Users エッジに向けてクエリを実行しています。 All は全件取得です。 UPDATE func (c *companyController) Update(w http.ResponseWriter, r *http.Request) { cId, err := strconv.Atoi(chi.URLParam(r, "companyId" )) // error handling company, err := c.dbc.Company.Get(c.ctx, cId) // error handling var req request.CompanyUpdateReq err := render.DecodeJSON(r.Body, &req) // error handling company, err = company.Update().SetName(req.Name).Save(c.ctx) // error handling w.WriteHeader(http.StatusOK) render.JSON(w, r, company) } // ---------- package request ...snip type CompanyUpdateReq struct { Name string `json:"name"` } 2021/10/20 09:27:14 driver.Query: query=SELECT DISTINCT `companies`.`id`, `companies`.`created_at`, `companies`.`updated_at`, `companies`.`name` FROM `companies` WHERE `companies`.`id` = ? LIMIT 2 args=[1] 2021/10/20 09:27:14 driver.Tx(5ca9d42e-1823-4956-8427-f9937f5fb5c6): started 2021/10/20 09:27:14 Tx(5ca9d42e-1823-4956-8427-f9937f5fb5c6).Exec: query=UPDATE `companies` SET `updated_at` = ?, `name` = ? WHERE `id` = ? args=[2021-10-20 09:27:14.615562 +0900 JST m=+1007.701267213 chan2 1] 2021/10/20 09:27:14 Tx(5ca9d42e-1823-4956-8427-f9937f5fb5c6).Query: query=SELECT `id`, `created_at`, `updated_at`, `name` FROM `companies` WHERE `id` = ? args=[1] 2021/10/20 09:27:14 Tx(5ca9d42e-1823-4956-8427-f9937f5fb5c6): committed Updateは受け取った id から企業を取得し、リクエストパラメーターを元に企業情報を更新するメソッドです。 まず、先ほどと同様に企業を取得します。 取得した企業モデルから Update を呼び出して更新用のクエリビルドを行います。後述の Set~ はセッターで最後の Save でクエリを実行しています。 DELETE func (c *companyController) Delete(w http.ResponseWriter, r *http.Request) { cId, err := strconv.Atoi(chi.URLParam(r, "companyId" )) // error handling company, err := c.dbc.Company.Get(c.ctx, cId) // error handling err = c.dbc.Company.DeleteOne(company).Exec(c.ctx) // error handling w.WriteHeader(http.StatusOK) render.JSON(w, r, fmt.Sprintf( "id=%d is deleted" , cId)) } 2021/10/20 09:31:41 driver.Query: query=SELECT DISTINCT `companies`.`id`, `companies`.`created_at`, `companies`.`updated_at`, `companies`.`name` FROM `companies` WHERE `companies`.`id` = ? LIMIT 2 args=[1] 2021/10/20 09:31:41 driver.Tx(55aded72-c284-490e-8096-8226edafc3f7): started 2021/10/20 09:31:41 Tx(55aded72-c284-490e-8096-8226edafc3f7).Exec: query=DELETE FROM `companies` WHERE `companies`.`id` = ? args=[1] 2021/10/20 09:31:41 Tx(55aded72-c284-490e-8096-8226edafc3f7): committed Delteは受け取った id から企業を取得し、該当企業を削除するメソッドです。 まず、先ほどと同様に企業を取得します。 *ent.Client(dbc) から DeleteOne で削除するレコードを指定し Exec で処理を実行しています。 CREATE(Transaction) func (c *companyController) CreateWithUser(w http.ResponseWriter, r *http.Request) { var req request.CompanyCreateWithUserReq err := render.DecodeJSON(r.Body, &req) // error handling tx, err := c.dbc.Tx(c.ctx) // error handling company, err := tx.Company. Create(). SetName(req.CompanyName). Save(c.ctx) if err != nil { err = util.Rollback(tx, err) // error handling } user, err := tx.User.Create(). SetCompany(company). SetName(req.UserName). SetEmail(req.UserEmail). SetRole(user.RoleAdmin). SetComment(req.UserComment). Save(c.ctx) if err != nil { err = util.Rollback(tx, err) // error handling } err = tx.Commit() // error handling w.WriteHeader(http.StatusOK) render.JSON(w, r, map [ string ] interface {}{ "company" : company, "user" : user, }) } // ---------- package request type CompanyCreateWithUserReq struct { CompanyName string `json:"companyName"` UserName string `json:"userName"` UserEmail string `json:"userEmail"` UserComment null.String `json:"userComment"` } // ---------- package util func Rollback(tx *ent.Tx, err error ) error { if rerr := tx.Rollback(); rerr != nil { err = fmt.Errorf( "%w: %v" , err, rerr) } return err } 2021/10/20 09:37:31 driver.Tx(66b33cc7-bf03-48ec-9ec1-029298c7e6c0): started 2021/10/20 09:37:31 Tx(66b33cc7-bf03-48ec-9ec1-029298c7e6c0).Exec: query=INSERT INTO `companies` (`created_at`, `updated_at`, `name`) VALUES (?, ?, ?) args=[2021-10-20 09:37:31.187128 +0900 JST m=+9.538263311 2021-10-20 09:37:31.187128 +0900 JST m=+9.538263623 nullcorp] 2021/10/20 09:37:31 Tx(66b33cc7-bf03-48ec-9ec1-029298c7e6c0).Exec: query=INSERT INTO `users` (`created_at`, `updated_at`, `name`, `email`, `role`, `comment`, `company_id`) VALUES (?, ?, ?, ?, ?, ?, ?) args=[2021-10-20 09:37:31.188519 +0900 JST m=+9.539654394 2021-10-20 09:37:31.188521 +0900 JST m=+9.539656522 a abcde@example.com admin {{ false}} 4] 2021/10/20 09:37:31 Tx(66b33cc7-bf03-48ec-9ec1-029298c7e6c0): committed CreateWithUserは受け取ったリクエストパラメーターから企業、ユーザーを作成するメソッドです。 まず、 *ent.Client(dbc) から Tx でトランザクションを作成し、トランザクション内で行なう処理を後述しています。 作成したトランザクションから *ent.Client(dbc) と同様に対象テーブルを指定し、 Create で作成用のクエリビルドを行い、更新処理と同様に Save で処理を実行しています。 error が返ってきた場合には util.Rollback でラップしてる tx.Rollback を実行しロールバックします。 最後に tx.Commit でトランザクションをコミットします。 CRUD通してどれも自動生成されたコードから簡単にクエリビルドができました。 今回利用した関数以外にも多くの関数が自動生成により用意されるため、少々複雑なクエリもそれらの組み合わせで構築できそうでした。 良かった点・もうひとつだった点 最後に ent を利用してみて感じた、良かった点・もうひとつだった点を挙げます。 良かった点 良かったと感じた点は以下になります。 自動生成機能の強力さ ドキュメントの充実度 機能の豊富さ 自動生成機能の強力さ やはりコレが一番のメリットに感じました。Repository層いらずと言いますか、初期段階でモデルに汎用関数が一通り揃っているため、新たに自前で拵えるコードの量は最小限で済みます。 スキーマの定義も比較的直感的にできそうでした。今回は初めて触ったということもあり実装に少々手こずる箇所もありましたが、慣れてしまえば効率良く実装できそうだと感じました。 また、カスタマイズ性が低い点が自動生成における懸念点かと思いますが、 ent にはスキーマの各メソッド定義に対して豊富にオプションが取り揃えられており、一般にORM利用時にネックになりやすいケースはどれもオプションでカバーされていそうでした。 スキーマからテーブル定義、モデルの定義の両方が行えるため、相互の間に定義のズレが生じることが無い点も管理の煩雑さが減って良いです。 ドキュメントの充実度 2020年v0.1.0発と比較的若いORMでありながら、公式ドキュメントが充実しており大抵の不明点はそこで解決できそうでした。日本語翻訳のドキュメントがあるのはとっても助かります。 機能の豊富さ 本記事で紹介しきれませんでしたが、一般的なORMで利用できる(トランザクション、Eagerローディング、フック、ページング)などの機能は ent でも一通りカバーされています。 また、GORMには組み込まれていないバリデーションなどの機能も ent には組み込まれています。 弊社のプロダクトでは、Gin + GORM構成だということもありGinにパックされている validator をモデルのバリデーションにも流用しています。 ent ではそこも1パックに利用できるため、ライブラリ間の橋渡し的な実装もする必要がなく便利でした。 他にも自動生成を独自テンプレートで行なうオプションなど拡張機能も用意されているようです。この辺りは今回調査できなかったので、またあらためて触ってみたいと思います。 もうひとつだった点 もうひとつに感じた点は以下になります。 Schemaの管理 Schemaの管理 これは ent 組み込みのAuto migration機能でプロダクションスキーマの管理が可能かという点での不満点です。 弊社のプロダクトではORMにはGORMを、マイグレーションツールには goose を利用しています。 ent ではGORMとは異なりAuto migrationによるリソースの削除ができます。しかし、バージョン管理なしに本番環境でAuto migrationを実行できるかというと少々心許ない気がします。 別途マイグレーションツールを導入して、 *migrate.Schema に用意されている WriteTo メソッドで一度DDLで導入したツールのマイグレーションファイルに定義を吐き出したうえで、本番ではそちらを実行する運用がありえそうでしょうか。 WriteTo のようなメソッドも用意されており便利ではありますが、ちょっと一手間では合ったためもうひとつの点として挙げました。 まとめ いかがでしたでしょうか。 本記事ではGoのORM ent についてご紹介いたしました。普段利用していた GORM とは勝手が違う部分が多く、新感覚で楽しめました。 まだまだ新しいライブラリなので今後の発展も楽しみです。 早くデファクトスタンダードが決まって、そちらへ倒れてしまいたい。という気持ちもありつつ、あれこれと色々なツールに触れてみての楽しさもあったりとどっちつかずな思いの秋の夜長です。 最後までお読みいただきありがとうございました!