こんにちは。スマートキャンプ エンジニアの中田です。
皆さんは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
は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
の場合は紐づきを矢印で表す有向グラフで構造化されます。
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のORMent
についてご紹介いたしました。普段利用していたGORM
とは勝手が違う部分が多く、新感覚で楽しめました。
まだまだ新しいライブラリなので今後の発展も楽しみです。
早くデファクトスタンダードが決まって、そちらへ倒れてしまいたい。という気持ちもありつつ、あれこれと色々なツールに触れてみての楽しさもあったりとどっちつかずな思いの秋の夜長です。
最後までお読みいただきありがとうございました!