SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

GoのFacebook製ORM"ent"を使ってみた

こんにちは。スマートキャンプ エンジニアの中田です。

皆さんは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」とは

前述したように、entFacebook 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%の静的な型付けが実現されています。

また、entGORM同様にドキュメントが充実しています。 日本語翻訳もされており、本記事の執筆にあたりentを実際に利用してみた際に生じた困りごとはほぼほぼ公式ドキュメントを参照すれば解決できました。

グラフ構造

entではグラフ構造に基づいてスキーマを定義していきます。

グラフ構造とは以下の図のようにノード(節点・頂点、点)の集合とエッジ(枝・辺、線)の集合で構成される構造のことです。このように構造化することでさまざまなオブジェクトの関連を表すことができます。

この図では参照元、参照先を表現しない無向グラフが書かれていますが、entの場合は紐づきを矢印で表す有向グラフで構造化されます。

f:id:smartcamp:20211020111011p:plain
グラフ(WikiPediaより)

WikiPedia - グラフ理論

entにおけるノードはモデル、エッジはモデルのリレーションを指します。

初期化時点でのスキーマには一つのノードに対して、FieldsEdgesメソッドが生えた状態でコードが生成されます。細かな定義方法は後述しますが、ここで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
---

以下のコマンドを実行して、UserCompanyスキーマファイルを自動生成します。

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.gocreated_atupdated_atの2フィールドをセットで切り出し、companyuserの両スキーマに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_idRequiredメソッドで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は受け取ったidcompaniesテーブルに検索をかけ、マッチしたレコードを取得するメソッドです。 *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同様に受け取ったidcompaniesテーブルに検索をかけ、取得した企業に属するユーザーの一覧を返すメソッドです。 まず、*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とは勝手が違う部分が多く、新感覚で楽しめました。 まだまだ新しいライブラリなので今後の発展も楽しみです。

早くデファクトスタンダードが決まって、そちらへ倒れてしまいたい。という気持ちもありつつ、あれこれと色々なツールに触れてみての楽しさもあったりとどっちつかずな思いの秋の夜長です。

最後までお読みいただきありがとうございました!

宣伝

スマートキャンプ株式会社では現在Webアプリケーションエンジニアを積極採用中です! 興味を持たれた方は以下のリンクをご覧ください!

smartcamp.co.jp