Go + Docker Composeを使ってEdgeDBを動かしてみた
はじめに
EdgeDB 1.0 がリリースされたことをTwitterで知って興味を持ったので、EchoでAPIサーバーを立てて動かしてみました。
今回作成したサンプルコードはこちらのGitHubリポジトリにあります。
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
と同じ内容を書くようにしましょう。
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層
type User struct {
Id edgedb.UUID `edgedb:"id"`
Username string `edgedb:"username"`
Password string `edgedb:"password"`
}
EdgeDBでは自動でUUID型のidが振られるので、Id
はUUID
型で指定します。
edgedb/edgedb-go
にはEdgeDBのスキーマで扱われる型が用意されていますので、今回はこちらを利用します。
Repository層
DBに対するCRUD操作を記述します。
1つのレコードを扱う場合は QuerySingle
メソッドを用います。
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
メソッドを使います。
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と異なる点があるので、そこを紹介したいと思います。基本的な書き方については公式ドキュメントをご参照ください。
id
のみ出力される
SELECTではデフォルトで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
を使って変換します。
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
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で書きます。
{
"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