GoのORM、entを使ってGremlinServerに繋げる
2021-03-15

#要約
- entとは
- entの利点
- GremlinServerに繋げる
#はじめに
ORM使っていますか?
ここ1年、GraphDB(AWS Neptune)を使うことがあり、GraphDBのクエリーやEntityをどのように扱うかを調べていたことがあります。その時は有力なGraphDBに対応したORMを見つけられなかったので、自前でGremlinというデータベース操作言語のQueryBuilderやORMを作っていましたが、Gremlinの仕様をすべてカバーするのは大変でした。
あとから、GraphDBに対応したORMがあることを知りました。
今回は、entをご紹介したいと思います。
#entとは
Simple, yet powerful entity framework for Go, that makes it easy to build and maintain applications with large data-models.
- Schema As Code - model any database schema as Go objects.
- Easily Traverse Any Graph - run queries, aggregations and traverse any graph structure easily.
- Statically Typed And Explicit API - 100% statically typed and explicit API using code generation.
- Multi Storage Driver - supports MySQL, PostgreSQL, SQLite and Gremlin.
- Extendable - simple to extend and customize using Go templates.
出典: ent - GitHub
特徴的なのは、Graphとしてデータを扱える点だとおもいます。
Storage DriverでMySQL
, PostgreSQL
, SQLite
and Gremlin
.に対応しています。
※Gremlin対応しているGraphDBはありますが、互換性については注意が必要です。また、2021-03-15時点ではExperimentsな機能となっています。
また、静的型付けであることも他のORMと異なる点だと思います。
go generate
で自動生成されたコードを使うことになります。
#entの利点
- 静的型付け・明示的
これまでは、gorm
を多用してきました。
gormが動的型付けで暗黙的なのに対して、entは静的型付けで明示的という点が対照的だと思いました。
gormはinterface{}
型を多用し、構造体のタグによってリレーションシップの解決を行ったりしますが、interface{}
型と構造体のタグは動的に解決されるため、例えば、構造体のタグにTypoがあってもコンパイルはできてしまいます。
entでも構造体のタグを指定する部分がないわけではないですが、基本的にはDBに対する指示をメソッドチェーンで追加していく形になりますので、Typoによるバグは減らせるかなと思いました。
- コード自動生成・自動補完
コードが自動生成されることによりエディターの補完が効くのも良いと思いました。
ORM自体のデバッグをする際にもReflectを多用しているgormより自動生成されたコードのほうが、デバッグしやすいようにも思います。
- コード削減
DDDで設計しているとテーブルごとにRepositoryを作っているのですが、entだと自動生成されるので、これをUsecaseで直接使っています。Repositoryの実装・テストがなくなるのでコード削減できます。DDD的には特定のライブラリに依存するので賛否があると思いますが。。
これらの特徴に関しては、好みだと思いますが、Typingの恩恵を受けられるほうが、安全なコーディングができて個人的には好みです。
#GremlinServerに繋げる
Gremlinのリファレンス実装である、Apache TinkerPop
のGremlinServerとつなげて動かしてみようと思います。
すべてのソースコードは、こちらに配置しています。
CloudShellでローカル環境を汚さずに試すことが可能です。
#前提条件
- docker
- docker-compose
- Go 1.16
#schemaをつくる
entgo.io
を参考にent cliを利用してSchemaの雛形を作ります。
mkdir workspace/project
cd workspace/project
go mod init
go get entgo.io/ent/cmd/ent
go run entgo.io/ent/cmd/ent init User
- ent/schema/user.go
下記の通りに編集します。
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
)
type User struct {
ent.Schema
}
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("id"),
field.String("name"),
}
}
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("friends", User.Type),
}
}
- ent/generate.go
storage
をgremlin
に指定して、gremlinで接続するためのコードをGenerateします。
AWS Neptuneでは、idの型がUUIDなので、互換のためidtype
をstring
に指定しています。
package ent
//go:generate go install entgo.io/ent/cmd/ent
//go:generate ent generate ./schema --storage gremlin --idtype string
#generateする
go generate ./...
entディレクトリ配下に自動生成されたコードが出力されます。
#GremlinServerを起動する
GremlinServerのConfigの簡単な変更内容の説明です。
docker-compose.ymlやconfはGitHubから入手できます。
- conf/server/my-gremlin-server.yaml
channelizerをWsAndHttpChannelizer
に変更。
entはWebsocketではなくHttp(REST)で接続しますので変更が必要でした。
- conf/server/my-tinkergraph.properties
idManagerをUUIDに変更。
AWS NeptuneのIDの型がUUIDなので互換を保つためです。
GremlinServerを起動します。
docker-compose build
docker-compose up -d
#実行
- main.go
これはUserVertex(name: bob)とUserVertex(name: alice)を作ります。
FriendEdgeとしてalice - friends -> bob
のように関係します。
package main
import (
"context"
"fmt"
"log"
"github.com/cloudandbuild/blog-article-resources/content/blog/article-3/ent"
)
func main() {
ctx := context.Background()
client, err := ent.Open("gremlin", "http://localhost:8182")
if err != nil {
log.Fatal(err)
}
defer client.Close()
bob, err := CreateUser(ctx, client, "bob")
if err != nil {
log.Fatalf("failed CreateUser err: %v", err)
}
_, err = CreateUser(ctx, client, "alice", bob)
if err != nil {
log.Fatalf("failed CreateUser err: %v", err)
}
users, err := client.User.Query().All(ctx)
if err != nil {
log.Fatalf("failed User.Query().All err: %v", err)
}
log.Println(users)
}
func CreateUser(ctx context.Context, client *ent.Client, name string, friends ...*ent.User) (*ent.User, error) {
u, err := client.User.
Create().
SetName(name).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed creating user: %w", err)
}
var ids []string
for _, friend := range friends {
ids = append(ids, friend.ID)
}
if len(ids) > 0 {
if _, err := u.Update().AddFriendIDs(ids...).Save(ctx); err != nil {
return nil, fmt.Errorf("failed updating user: %w", err)
}
}
return u, nil
}
実行してみます。UserのQuery結果が表示されると思います。
go run main.go
#GremlinConsole
sqlでpsql
などCLIでConsoleが提供されているようにGremlinでもConsoleを使ってDBにアクセスすることができます。
gremlin-consoleを開く
docker-compose exec gremlin-console /bin/bash -c bin/gremlin.sh
gremlin-serverに接続する。
:remote connect tinkerpop.server /opt/gremlin/conf/remote.yaml
:remote console
queryを実行する。
gremlin> g.V().valueMap(true)
==>{id=3c48f9e7-2ad0-403c-9398-96101b6d2cbf, label=user, name=[bob]}
==>{id=cd3900cf-79fe-4bde-8a55-8c840ce9d8e3, label=user, name=[alice]}
gremlin> g.E()
==>e[e7ca2294-a11d-4ffc-8a98-22ef1196df47][cd3900cf-79fe-4bde-8a55-8c840ce9d8e3-user_friends->3c48f9e7-2ad0-403c-9398-96101b6d2cbf]
aliceの友達は、bobですね。
#まとめ
今回は、entを使ってGremlinServerに繋げることを紹介しました。
entやGremlinは利用して知見がたまってきたので、その他にも色々とご紹介できればと思います。
Graphの可視化や具体的な実用例を別途記事にできればと思います。
