Go言語のグラフデータモデル用ORM ent が良かった
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)]
まとめ
いかがでしょうか。 まだ実験段階のため本番利用は早い気がしますが、今後の選択肢としては十分ありかと思います。