
Go言語のグラフデータモデル用ORM ent が良かった
この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
GoでGremlin用のORMがなくてつらかったのですが、entというオープンソースプロジェクトをみつけたので試してみました。
FacebookでGo用のグラフベースのORMとして利用されていたフレームワークEntが元になっています。
グラフ構造のデータをスキーマとして定義することで、スキーマからのGoのコードを自動生成してくれます。
MySQL/MariaDB/PostgreSQL/SQLite/Gremlim(considered experimental) などのデータベースのデータベースに対応しており、GraphQLのバックエンドとしてしても利用できます。
執筆時点で、最新はv0.8.0ながらGitHubの更新も頻繁で期待できそうです。
v1までのロードマップも公開されています。
今回はこちらのentがGremlinの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)]
まとめ
いかがでしょうか。 まだ実験段階のため本番利用は早い気がしますが、今後の選択肢としては十分ありかと思います。







