Go言語のグラフデータモデル用ORM ent が良かった

2021.06.23

GoでGremlin用のORMがなくてつらかったのですが、entというオープンソースプロジェクトをみつけたので試してみました。

FacebookでGo用のグラフベースのORMとして利用されていたフレームワークEntが元になっています。

グラフ構造のデータをスキーマとして定義することで、スキーマからのGoのコードを自動生成してくれます。

MySQL/MariaDB/PostgreSQL/SQLite/Gremlim(considered experimental) などのデータベースのデータベースに対応しており、GraphQLのバックエンドとしてしても利用できます。

執筆時点で、最新はv0.8.0ながらGitHubの更新も頻繁で期待できそうです。

v1までのロードマップも公開されています。

今回はこちらのentGremlinのORMとして利用できそうか調査してみました。

はじめに結論

本番利用は少し待ったほうが良さそうという印象です。

Gremlinの対応が実験的と書いているとおり、不具合っぽいところが見つかりました。

ただ、問い合わせると即日でPR出して修正してくれました。( ゚Д゚)

コミュニティが活発なので、今後GremlinないしAmazon NeptuneのORMとしての利用が期待できそうです。

せっかちな人へ

GitHubを参照してください。

やってみる

公式のQuick Introductionを参考にして、Gremlinように少し修正を加えています。

セットアップ

  • 初期化
go mod init gremlin-orm-sample
  • モジュールの追加
#  ※masterの最新が必要なため
go get entgo.io/ent/cmd/ent/@master
go get github.com/google/uuid

スキーマを作成してみる

  • スキーマの追加
go run entgo.io/ent/cmd/ent init User
  • スキーマの修正 schema/user.go
// Fields of the User.
func (User) Fields() []ent.Field {
 return []ent.Field{
  field.String("id").
   NotEmpty().
   Unique().
   Immutable(),
  field.Int("age").
   Positive(),
  field.String("name").
   Default("unknown"),
 }
}
  • ent/generate.goの末尾に下記を追加

--storage gremlin --idtype string

  • コード自動生成
go generate ./ent

エンティティを作成してみる

  • main.goを作成
package main

import (
 "context"
 "fmt"
 "gremlin-orm-sample/ent"
 "gremlin-orm-sample/ent/user"
 "log"

 "entgo.io/ent/dialect"
 "github.com/google/uuid"
)

func main() {
 var err error
 client, err := ent.Open(dialect.Gremlin, "http://localhost:8182")
 if err != nil {
  log.Fatalf("creating client: %v", err)
 }
 defer client.Close()

 ctx := context.Background()

 user, err := CreateUser(ctx, client, uuid.New().String(), 32, "seiichi")
 if err != nil {
  log.Fatalf(err.Error())
 } else {
  log.Println("created user: ", user)
 }

 users, err := SearchUsersById(ctx, client, user.ID)
 if err != nil {
  log.Fatalf(err.Error())
 } else {
  log.Println("searched users by id: ", users)
 }
}

func CreateUser(ctx context.Context, client *ent.Client, id string, age int, name string) (*ent.User, error) {
 user, err := client.User.
  Create().
  SetID(id).
  SetAge(age).
  SetName(name).
  Save(ctx)
 if err != nil {
  return nil, fmt.Errorf("failed creating user: %w", err)
 }
 return user, nil
}

func SearchUsersById(ctx context.Context, client *ent.Client, id string) ([]*ent.User, error) {
 users, err := client.User.
  Query().
  Where(user.IDEQ(id)).
  All(ctx)
 if err != nil {
  return nil, fmt.Errorf("failed querying user: %w", err)
 }
 return users, nil
}
  • コード実行
# For Go 1.16
$ go get -t .
$ go run main.go
created user:  User(id=e6b49139-9067-4b90-aaed-aedae723d73a, age=32, name=seiichi)
searched users by id:  [User(id=e6b49139-9067-4b90-aaed-aedae723d73a, age=32, name=seiichi)]

※事前にGremlin Serverを立ち上げておく必要があります。詳細はGitHubを参照してください。

エッジを追加してみる

  • スキーマの追加
go run entgo.io/ent/cmd/ent init Car
  • スキーマの修正 schema/user.go

edgeを追加しています。

// Edges of the User.
func (User) Edges() []ent.Edge {
 return []ent.Edge{
  edge.To("cars", Car.Type),
 }
}
  • スキーマの修正 schema/car.go
// Fields of the Car.
func (Car) Fields() []ent.Field {
 return []ent.Field{
  field.String("id").
   NotEmpty().
   Unique().
   Immutable(),
  field.String("model"),
  field.Time("registered_at"),
 }
}
  • コード再生成
go generate ./ent
  • main.goの修正

ユーザーにエッジを追加しています。

package main

import (
 "context"
 "fmt"
 "gremlin-orm-sample/ent"
 "gremlin-orm-sample/ent/car"
 "gremlin-orm-sample/ent/user"
 "log"
 "time"

 "entgo.io/ent/dialect"
 "github.com/google/uuid"
)

func main() {
 var err error
 client, err := ent.Open(dialect.Gremlin, "http://localhost:8182")
 if err != nil {
  log.Fatalf("creating client: %v", err)
 }
 defer client.Close()

 ctx := context.Background()

 user, err := CreateUser(ctx, client, uuid.New().String(), 32, "seiichi")
 if err != nil {
  log.Fatalf(err.Error())
 } else {
  log.Println("created user: ", user)
 }

 users, err := SearchUsersById(ctx, client, user.ID)
 if err != nil {
  log.Fatalf(err.Error())
 } else {
  log.Println("searched users by id: ", users)
 }

 car, err := CreateCar(ctx, client, uuid.New().String(), "Tesla")
 if err != nil {
  log.Fatalf(err.Error())
 } else {
  log.Println("created car: ", car)
 }

 newUser, err := AddCarToUser(ctx, client, user, car)
 if err != nil {
  log.Fatalf(err.Error())
 } else {
  log.Println("add car to user: ", newUser)
 }

 newUsers, err := SearchUsersByCarModel(ctx, client, "Tesla")
 if err != nil {
  log.Fatalf(err.Error())
 } else {
  log.Println("searched user by car model: ", newUsers)
 }
}

func CreateUser(ctx context.Context, client *ent.Client, id string, age int, name string) (*ent.User, error) {
 user, err := client.User.
  Create().
  SetID(id).
  SetAge(age).
  SetName(name).
  Save(ctx)
 if err != nil {
  return nil, fmt.Errorf("failed creating user: %w", err)
 }
 return user, nil
}

func SearchUsersById(ctx context.Context, client *ent.Client, id string) ([]*ent.User, error) {
 users, err := client.User.
  Query().
  Where(user.IDEQ(id)).
  All(ctx)
 if err != nil {
  return nil, fmt.Errorf("failed querying user: %w", err)
 }
 return users, nil
}

func CreateCar(ctx context.Context, client *ent.Client, id string, name string) (*ent.Car, error) {
 car, err := client.Car.
  Create().
  SetID(id).
  SetModel(name).
  SetRegisteredAt(time.Now()).
  Save(ctx)
 if err != nil {
  return nil, fmt.Errorf("failed creating car: %w", err)
 }
 return car, nil
}

func AddCarToUser(ctx context.Context, client *ent.Client, user*ent.User, car *ent.Car) (*ent.User, error) {
 user, err := user.Update().
  AddCars(car).
  Save(ctx)
 if err != nil {
  return nil, fmt.Errorf("failed add car to user: %w", err)
 }
 return user, nil
}

func SearchUsersByCarModel(ctx context.Context, client *ent.Client, carModel string) ([]*ent.User, error) {
 users, err := client.User.
  Query().
  Where(user.HasCarsWith(car.Model(carModel))).
  All(ctx)
 if err != nil {
  return nil, fmt.Errorf("failed querying user cars: %w", err)
 }
 return users, nil
}
  • コード実行
$ go run main.go
created user:  User(id=7ba08139-edb6-4202-8d3f-dacecf1678b0, age=32, name=seiichi)
searched users by id:  [User(id=7ba08139-edb6-4202-8d3f-dacecf1678b0, age=32, name=seiichi)]
created car:  Car(id=39aff5fe-e9a2-4c0c-98e5-883b202818a7, model=Tesla, registered_at=Wed Jun 23 11:55:58 2021)
add car to user:  User(id=7ba08139-edb6-4202-8d3f-dacecf1678b0, age=32, name=seiichi)
searched user by car model:  [User(id=7ba08139-edb6-4202-8d3f-dacecf1678b0, age=32, name=seiichi)]

まとめ

いかがでしょうか。 まだ実験段階のため本番利用は早い気がしますが、今後の選択肢としては十分ありかと思います。