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

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

要約

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

Open in Cloud Shell

前提条件

  • 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

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

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