🐥

Go + Docker Composeを使ってEdgeDBを動かしてみた

2022/02/14に公開

はじめに

EdgeDB 1.0 がリリースされたことをTwitterで知って興味を持ったので、EchoでAPIサーバーを立てて動かしてみました。
今回作成したサンプルコードはこちらのGitHubリポジトリにあります。

https://github.com/tatsuya0429/EdgeDB-Golang-Docker-Sample

EdgeDBとは

GitHubのREADMEを見ると、

EdgeDB is a new kind of database that takes the best parts of relational databases, graph databases, and ORMs. We call it a graph-relational database.

EdgeDBは、リレーショナルデータベース、グラフデータベース、ORMの良いところを取り入れた新しいタイプのデータベースです。私たちはこれを「グラフリレーショナルデータベース」と呼んでいます。(DeepL翻訳)

と書いてあります。

また公式ドキュメントでは、

  • 厳密で強力な型スキーマがある
  • 強力でクリーンなクエリ言語がある
  • 複雑な階層データに対しても簡単に扱える
  • マイグレーションのサポートもある

という特徴があると書かれています。

環境

  • WSL2(Ubuntu 20.04)
  • Go 1.16
  • Echo v4
  • edgedb/edgedb-go v0.9.2
    • EdgeDB公式のGoのDB Clientライブラリです。Goの他にPythonやJavaScript/TypeScritptのClientライブラリもあります。

フォルダ構成

.
├── Dockerfile
├── credentials // EdgeDBの認証関連
│   └── local_dev.json
├── docker-compose.yml
├── go.mod
├── go.sum
├── main.go
├── schema // EdgeDBのスキーマ
│   ├── default.esdl
│   └── migrations  // マイグレーションファイル
│       └── 00001.edgeql
└── src
    ├── handlers  // Controller層
    │   ├── todo_handler.go
    │   └── user_handler.go
    ├── infrastructure // Infrastructure層
    │   ├── config.go
    │   └── dbclient.go
    ├── models // Domain層
    │   ├── todo.go
    │   └── user.go
    └── repositories // Repository層
        ├── todo_repository.go
        └── user_repositories.go

手順

EdgeDBのスキーマを定義

EdgeDBではスキーマをSDL(EdgeDB Schema Definition Language)で表現します。
オブジェクト指向言語っぽく書けて、構造化しにくいデータでも比較的扱いやすくなっています。

テーブルの定義は、以下のようにtypeで書くことができます。

type User {
  required property username -> str {
    constraint exclusive;  // unique制約
  }  // カラムの制約を書きたい場合は{}をつけて記述できます。
  required property password -> str;
}

EdgeDBでは、テーブル定義をmodule内で行う必要があります。このmoduleではテーブルだけでなく、関数やエイリアスも定義することができます。個人的にGoのPackageみたいなものかなと思っています。サービスの境界とかもmoduleで表現できそうですね。

module default {
  type User {
    ...
  }

  type Todo {
    required property title -> str;
    property description -> str;
    property status -> str {
      constraint one_of('Todo', 'WIP', 'Done'); // DjangoのORMのCHOICEと似ています。
      default := 'Todo'; // Default 値
    }
    property deadline -> cal::local_date; // date型を格納できます。
    link user -> User;
  }
}

Go EchoでAPIサーバーを作成

ここではEdgeDBに関連するところのみを取り上げたいと思います。

Infrastructure層

DB Clientを定義します。edgedb.Optionsについては、後述するcredentialsと同じ内容を書くようにしましょう。

infrastructure/dbclient.go
func NewDBClient(ctx context.Context) *edgedb.Client {
	config := NewDBConfig()
	opts := edgedb.Options{
		Database: config.DBName,
		Host: config.Host,
		User: config.User,
		Password: edgedb.NewOptionalStr(config.Password),
		Port: config.Port,
		TLSOptions: edgedb.TLSOptions{
			SecurityMode: edgedb.TLSModeInsecure,
		},
		Concurrency: 4,
	}
	client, err := edgedb.CreateClient(ctx, opts)
	if err != nil {
		log.Fatal(err)
	}
	return client
}

Domain層

models/user.go
type User struct {
	Id       edgedb.UUID `edgedb:"id"`
	Username string      `edgedb:"username"`
	Password string      `edgedb:"password"`
}

EdgeDBでは自動でUUID型のidが振られるので、IdUUID型で指定します。
edgedb/edgedb-goにはEdgeDBのスキーマで扱われる型が用意されていますので、今回はこちらを利用します。

Repository層

DBに対するCRUD操作を記述します。

1つのレコードを扱う場合は QuerySingleメソッドを用います。

repositories/user_repository.go
func (repo *UserRepository) CreateUser(ctx context.Context, username string, password string) error {
	client := infrastructure.NewDBClient(ctx)
	defer client.Close()
	var inserted struct{ id edgedb.UUID }  // 出力用の変数
	query := `INSERT User {
		username := <str>$0, 
		password := <str>$1
	}`
	err := client.QuerySingle(ctx, query, &inserted, username, password)
	if err != nil {
		return err
	}
	return nil
}

複数レコードを扱う場合は、Queryメソッドを使います。

repositories/user_repository.go
func (repo *UserRepository) GetAll(ctx context.Context) (users []models.User, err error) {
	client := infrastructure.NewDBClient(ctx)
	query := "SELECT User{id, username, password}"
	err = client.Query(ctx, query, &users)
	if err != nil {
		return
	}
	return
}

EdgeQLについて

EdgeDBでは、EdgeQLという独自のクエリ言語が用意されています。SQLと書き方は似ているので理解しやすいと思いますが、一部SQLと異なる点があるので、そこを紹介したいと思います。基本的な書き方については公式ドキュメントをご参照ください。

SELECTではデフォルトでidのみ出力される

EdgeQLではSELECT Userというクエリだとidのみが出力されるようになっています。
そのためid以外のカラムも出力したい場合は、SELECT User{id, username, password}というようにカラムを明示する必要があります。

パラメータ参照

EdgeQLではパラメータを参照することができます。上記の例をみると

INSERT User {
  username := <str>$0, 
  password := <str>$1
}

というように<型>$パラメータ名でパラメータを表記します。

独自の型に対する扱い

edgedb/edgedb-goにはEdgeDBで用いる独自の型について用意されており、一部Goの標準の型から変換する必要があります。例えば、日付を表すcal::local_date型のカラムはtime.Timeからedgedb.LocalDateに変換しないと格納できません。そのためedgedb.NewLocalDateを使って変換します。

repositories/todo_repository.go
func (repo *TodoRepository) CreateTodo(ctx context.Context, title string, description string, deadlineStr string, userIdStr string) error {
	client := infrastructure.NewDBClient(ctx)
	defer client.Close()
	var inserted struct{ id edgedb.UUID }
	deadline, err := utils.StringToTime(deadlineStr)
	if err != nil {
		return err
	}
	localdate := edgedb.NewLocalDate(deadline.Date())
	if err != nil {
		return err
	}
	userId, err := edgedb.ParseUUID(userIdStr)
	if err != nil {
		return err
	}
	query := `
		INSERT Todo {
			title := <str>$0, 
			description := <str>$1,
			deadline := <cal::local_date>$2,
			user := (SELECT User FILTER .id = <uuid>$3)
		}
	`
	err = client.QuerySingle(ctx, query, &inserted, title, description, localdate, userId)
	if err != nil {
		return err
	}
	return nil
}

Dockerの準備

Goに関するDockerの設定についてはここでは書かず、EdgeDBに関する設定について説明します。

Docker Compose

docker-compose.yml
services:
  server:
    ...
  db:
    image: edgedb/edgedb
    environment:
      - EDGEDB_SERVER_SECURITY=insecure_dev_mode
      - EDGEDB_SERVER_DATABASE=sample
      - EDGEDB_SERVER_PASSWORD=development
    volumes:
      - ./schema:/dbschema
      - ./credentials:/root/.config/edgedb/credentials
      - edgedb_data:/var/lib/edgedb/data
    ports:
      - "5656:5656"

volumes:
  edgedb_data:

credentials

EdgeDBを外部から動かすにはcredential情報が必要なので、jsonで書きます。

credentials/local_dev.json
{
  "port": 5656,
  "database": "sample",
  "user": "edgedb",
  "password": "development",
  "host": "db",
  "tls_security": "insecure"
}

起動

準備が整ったので、コンテナを起動してみます。

$ docker-compose up -d --build

マイグレーション

EdgeDBにはマイグレーションの機能があるので実行してみます。

$ docker-compose exec db edgedb -I local_dev migrate

確認

$ curl http://localhost:5000/todos
[{"ID":"bc5480b0-8d6a-11ec-92aa-671f3e9e8dc8","Title":"sample","Description":"","Status":"Todo","Deadline":"2022-02-20","User":{"Id":"13593254-8cec-11ec-86ad-8b77027fbe60","Username":"testuser","Password":""}},{"ID":"6d500386-8d79-11ec-92dd-bb455e451356","Title":"sample2","Description":"","Status":"Todo","Deadline":"2022-02-20","User":{"Id":"13593254-8cec-11ec-86ad-8b77027fbe60","Username":"testuser","Password":""}}]

ちゃんと出力されていますね。

まとめ

今回はEcho + EdgeDB + Dockerを使って簡単なデータモデルで運用できるか試してみました。
個人的にはRDBでデータ構造を考えるよりも直観的に表現できて使いやすいなと思いました。
今度は再帰的な構造など複雑な階層データでもできるのか試してみたいと思います。

ここまで読んでいただきありがとうございました。

Discussion