GoのORM、entを使ってGremlinServerに繋げる

2021-03-15

egonelbre/gophersによるGitHubからの画像

#要約

#はじめに

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でローカル環境を汚さずに試すことが可能です。

Open in Cloud Shell

#前提条件

#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

下記の通りに編集します。

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),
	}
}

storagegremlinに指定して、gremlinで接続するためのコードをGenerateします。

AWS Neptuneでは、idの型がUUIDなので、互換のためidtypestringに指定しています。

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から入手できます。

channelizerをWsAndHttpChannelizerに変更。

entはWebsocketではなくHttp(REST)で接続しますので変更が必要でした。

idManagerをUUIDに変更。

AWS NeptuneのIDの型がUUIDなので互換を保つためです。

GremlinServerを起動します。

docker-compose build
docker-compose up -d

#実行

これは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の可視化や具体的な実用例を別途記事にできればと思います。