史上最強のデータベース、SurrealDB

SurrealDBというRust製データベースを知ったので紹介します。このデータベースはすごいです。リレーショナル、ドキュメント、グラフ、あらゆる種類のデータ構造を扱うことができ、かつインメモリ、単一ノード、分散環境、全てで動かすことができます。さらにHTTPやWebSocketによるアクセスと柔軟なユーザ認証、認可機能とがDB本体に内包されており、ブラウザから直に接続するWebDBとしても使えます。とにかくなんでもできる夢のデータベースといった感じです。

https://surrealdb.com/


# 特徴
機能を挙げていたら多くなりすぎたので、特に面白い部分を挙げます。

- 配列やオブジェクトをネストした複雑なデータ構造を持てるのに、レコードリンクという機能によりリレーションに対応していてしかもSQLやMongoDBより簡潔にクエリが書ける。
- スキーマレスで各レコードには任意のフィールドを持てるが、必要ならスキーマを定義することもできる。さらに各フィールドの値に式を使って制約を設けることも可能。
- DB本体にHTTPでの接続機能やユーザ認証・認可機能が含まれており、ブラウザから直接接続するWebDBとしても使える。さらにWebSocketで接続すれば別の接続からのDBの更新を即座に反映できるリアルタイムなDBにもなる。
- 組み込み関数が豊富でクエリ内でHTTPリクエストを行うことすら可能、かつ自分でJavaScriptを使って関数を定義することも可能であり、バックエンドの機能をDB内に取り込むことができる。
- 単一ノードはもちろん分散した複数ノードで動かすことも可能。さらにインメモリでも動く。



# 機能

公式サイトの以下にも書いてある。

https://surrealdb.com/features

## 動作環境
- 単一ノード
- 分散した複数ノード
- インメモリの単一ノード

## データ構造

- テーブル、レコード、フィールドからなる（ドキュメントDBと同じ）
- テーブルにスキーマを定義でき、スキーマに合致しないレコードを弾くことが可能な一方、スキーマを使わず任意の構造のレコードを入れられるようにもできる
  - RDB的にもドキュメントDB的にも使える
- テーブルのフィールドの値に式を使って制約をつけられる
- フィールドには配列型やオブジェクト型（文字列をキーとした辞書型）そしてそれをネストさせた複雑な構造を持たせされる
- レコードリンク型を使うことでRDBの外部キー的なことができる
  - ただし、外部キー制約の検証は多分できない
- グラフDBのようにレコード間の関係をグラフで表現できる
  - 例えば二人の人物をレコードで表し二人が知り合い同士ならレコード間に辺（エッジ）を追加する、辺には知り合った日付が付与される、といったデータ構造
  - これを使えばレコード間の多対多の関係を直接扱える
    - RDBのように中間テーブルを使って自分で実装する必要はない
- GeoJSON(直線や平面を表現する)型を扱える
- クエリ時に算出される読み取り専用フィールドを定義できる
  - 例えば生年月日からクエリした時点の年齢を計算して返すとか

## 問い合わせ
- SurrealQLというSQLを拡張したような独自の言語を使う
- レコードリンクを使った関係クエリ（SQLでのJOIN）
  - 多対多関係や複数のリレーションを使った深い関係クエリでもSQLより短く簡潔に記述できる
- トランザクションが使える
  - 単純に`BEGIN TRANSACTION;`と`COMMIT TRANSACTION;`で括れば複数の操作を1トランザクションにできる
- 変更操作を行なったときに差分を返すことができる
- 変更がなされた時にイベントを発報できる
- IF文による条件分岐が使える

## 関数
- DB内の式で使える関数が充実している
  - GeoJSONで平面の面積を計算する関数、フィールドの値制約で便利な文字列がメールアドレスかどうかを返す関数、HTTPリクエストを送って結果を返す関数もある
- JavaScriptで独自のロジックを組み込める

## 認証・認可
- 式を使った柔軟なロジックでユーザ認証をDB内で行える
- 式を使ってテーブルごとに各レコードのログイン中のユーザに対する読み取り、追加、更新、削除の可否を柔軟に決定できる

## 接続方法
- HTTP上でSurrealQL
- HTTP上でGraphQL（開発中）
- WebSocket
  - DBの内容の変更時にイベントがくる（リアルタイムなDB）
- ブラウザ等からアクセスするGUIの管理画面（開発中）

# いじってみる

以下の公式ドキュメントを参考に行った。

https://surrealdb.com/docs/start

## 起動
SurrealDBをユーザ: root、パスワード: rootで起動する。Dockerを使えば一発。

```console
$ docker run --rm -p 8000:8000 surrealdb/surrealdb:latest start --user root --pass root
```

## 接続
HTTPで接続できるのでcurlコマンドで良いのだが、楽をするためにGUIのHTTPクライアントを使った。参考までに私はInsomniaを使った。

https://insomnia.rest

## HTTPリクエスト

- URL: `http://localhost:8000/sql`
- メソッド: POST
- Basic認証: ユーザ: root、パスワード: root
- ヘッダ
  - `Accept: application/json`
  - `NS: test`
  - `DB: test`
- ボディ: SurrealQLの問い合わせ文

HTTPヘッダの`NS`と`DB`は名前空間とデータベース名を指定している。データベース名はRDBと同じ概念で複数のテーブルをまとめたものである。SurrealDBではさらに複数のデータベースをまとめた名前空間という概念が存在する。

## 問い合わせ例

上記のHTTPリクエストのボディ部分に各SurrealQLの問い合わせ文を入れて送る。

### 接続テスト

```sql:SurrealQL
INFO FOR DB;
```

```json:返答
[
	{
		"time": "110.118µs",
		"status": "OK",
		"result": {
			"dl": {},
			"dt": {},
			"sc": {},
			"tb": {}
		}
	}
]
```

### レコード追加

ドキュメントDBと同じく**テーブルやフィールドの定義を事前に作成する必要はない**。一方で**必要ならスキーマや値制約を定義することも可能**。ここではそれらの機能は使わないで行う。

```sql:SurrealQL
CREATE account SET
	name = 'ACME Inc',
	created_at = time::now()
;
```

```json:返答
[
	{
		"time": "11.469948ms",
		"status": "OK",
		"result": [
			{
				"created_at": "2022-10-18T03:02:11.384926367Z",
				"id": "account:5h9wuul5zogoeavmckzv",
				"name": "ACME Inc"
			}
		]
	}
]
```

返答を見てわかるように、レコードにはID（上記では`account:5h9wuul5zogoeavmckzv`）が振られる。**IDにはテーブル名が含まれる**ことが特徴で、SurrealDBではこの**IDを使って直接レコードにアクセスできる**。IDは明示的に指定することもできる。

```sql:SurrealQL
CREATE author:john SET
	name.first = 'John',
	name.last = 'Adams',
	name.full = string::join(' ', name.first, name.last),
	age = 29,
	admin = true,
	signup_at = time::now()
;
```

```json:返答
[
	{
		"time": "479.957µs",
		"status": "OK",
		"result": [
			{
				"admin": true,
				"age": 29,
				"id": "author:john",
				"name": {
					"first": "John",
					"full": "John Adams",
					"last": "Adams"
				},
				"signup_at": "2022-10-18T03:09:23.304462683Z"
			}
		]
	}
]
```

ブログの記事を作ってみよう。先ほど作った著者とアカウントを記事に関連させる。下記の例では著者の**テーブル名を含んだレコードIDを直接指定（レコードリンク）** している。アカウントのレコードリンクはサブクエリを使って指定している。

```sql:SurrealQL
CREATE article SET
	created_at = time::now(),
	author = author:john,
	title = 'Lorem ipsum dolor',
	text = 'Donec eleifend, nunc vitae commodo accumsan, mauris est fringilla.',
	account = (SELECT id FROM account WHERE name = 'ACME Inc' LIMIT 1)
;
```

```json:返答
[
	{
		"time": "2.360989ms",
		"status": "OK",
		"result": [
			{
				"account": "account:5h9wuul5zogoeavmckzv",
				"author": "author:john",
				"created_at": "2022-10-18T03:23:20.987274481Z",
				"id": "article:l6fvtj92ex720r1n8239",
				"text": "Donec eleifend, nunc vitae commodo accumsan, mauris est fringilla.",
				"title": "Lorem ipsum dolor"
			}
		]
	}
]
```

### レコード取得

```sql:SurrealQL
SELECT * FROM article;
```

```json:返答
[
	{
		"time": "1.255926ms",
		"status": "OK",
		"result": [
			{
				"account": "account:5h9wuul5zogoeavmckzv",
				"author": "author:john",
				"created_at": "2022-10-18T03:23:20.987274481Z",
				"id": "article:l6fvtj92ex720r1n8239",
				"text": "Donec eleifend, nunc vitae commodo accumsan, mauris est fringilla.",
				"title": "Lorem ipsum dolor"
			}
		]
	}
]
```

SurrealDBでは複数のテーブルから同時にレコードを取得できる。

```sql:SurrealQL
SELECT * FROM article, account;
```

```json:返答
[
	{
		"time": "119.372µs",
		"status": "OK",
		"result": [
			{
				"account": "account:5h9wuul5zogoeavmckzv",
				"author": "author:john",
				"created_at": "2022-10-18T03:23:20.987274481Z",
				"id": "article:l6fvtj92ex720r1n8239",
				"text": "Donec eleifend, nunc vitae commodo accumsan, mauris est fringilla.",
				"title": "Lorem ipsum dolor"
			},
			{
				"created_at": "2022-10-18T03:02:11.384926367Z",
				"id": "account:5h9wuul5zogoeavmckzv",
				"name": "ACME Inc"
			}
		]
	}
]
```

SurrealQLの特徴の一つに**リレーションを辿るのが非常に簡単**という点がある。以下の例では関係先の別テーブルのレコードのフィールドに対して条件をつけて、さらに関係先のレコードをIDではなくその場に展開して返すように指示している。

```sql:SurrealQL
SELECT * FROM article WHERE author.age < 30 FETCH author, account;
```

```json:返答
[
	{
		"time": "475.681µs",
		"status": "OK",
		"result": [
			{
				"account": {
					"created_at": "2022-10-18T03:02:11.384926367Z",
					"id": "account:5h9wuul5zogoeavmckzv",
					"name": "ACME Inc"
				},
				"author": {
					"admin": true,
					"age": 29,
					"id": "author:john",
					"name": {
						"first": "John",
						"full": "John Adams",
						"last": "Adams"
					},
					"signup_at": "2022-10-18T03:09:23.304462683Z"
				},
				"created_at": "2022-10-18T03:23:20.987274481Z",
				"id": "article:l6fvtj92ex720r1n8239",
				"text": "Donec eleifend, nunc vitae commodo accumsan, mauris est fringilla.",
				"title": "Lorem ipsum dolor"
			}
		]
	}
]
```

### グラフ

メールを表すレコードを追加し、そのメールが著者に送られたことを表現する辺を追加する。グラフの辺の追加には`RELATE`文を用いる。

```sql:SurrealQL
CREATE email:hoge SET 
  subject = 'Hoge',
  text = 'hoge fuga piyo'
;
RELATE email:hoge->to->author:john SET
  opened = false
;
```

```json:返答
[
	{
		"time": "518.011µs",
		"status": "OK",
		"result": [
			{
				"id": "email:hoge",
				"subject": "Hoge",
				"text": "hoge fuga piyo"
			}
		]
	},
	{
		"time": "2.689115ms",
		"status": "OK",
		"result": [
			{
				"id": "to:czoy6a5pqzbtsf2ipiyj",
				"in": "email:hoge",
				"opened": false,
				"out": "author:john"
			}
		]
	}
]
```

さらに別の著者を追加し、このメールの送信者であることを表現する。レコード追加には`CONTENT`を使った別の書き方をしている。

```sql:SurrealQL
CREATE author:jane CONTENT {
  name: { first: 'Jane', last: 'Smith' }
};
RELATE author:jane->send->email:hoge CONTENT {
  time: '2022-10-18T07:31:49Z',
};
```

```json:返答
[
	{
		"time": "230.283µs",
		"status": "OK",
		"result": [
			{
				"id": "author:jane",
				"name": {
					"first": "Jane",
					"last": "Smith"
				}
			}
		]
	},
	{
		"time": "113.366µs",
		"status": "OK",
		"result": [
			{
				"id": "send:apm1mvavmlpr0jkkn9s4",
				"in": "author:jane",
				"out": "email:hoge",
				"time": "2022-10-18T07:31:49Z"
			}
		]
	}
]
```

まだ`Jane`からのメールを開いていない受信者でかつ管理者を全て取得する。

```sql:SurrealQL
SELECT ->send->(email as email)->(to WHERE opened = false)->(author WHERE admin = true as receiver) FROM author:jane FETCH email, receiver
```

```json:返答
[
	{
		"time": "313.026µs",
		"status": "OK",
		"result": [
			{
				"email": [
					{
						"id": "email:hoge",
						"subject": "Hoge",
						"text": "hoge fuga piyo"
					}
				],
				"receiver": [
					{
						"admin": true,
						"age": 29,
						"id": "author:john",
						"name": {
							"first": "John",
							"full": "John Adams",
							"last": "Adams"
						},
						"signup_at": "2022-10-18T03:09:23.304462683Z"
					}
				]
			}
		]
	}
]
```

他にもフィールドの値制約やユーザ認証・認可やWebSocketによる変更通知など面白そうな機能がたくさんありますが、全部は書いてられないのでここまでにします。皆さんも試してみてください。

# あとがき
公式サイトでも謳っている通り、アルティメットなデータベースです。公式のドキュメントがまだ少なくて具体的なことを書いてない部分がたくさんありますが、凄そうなのは確かでワクワクしてきます。あとはパフォーマンスがどうなのかという点ですね。ただパフォーマンスよりも扱いやすさ、システムの単純さが決め手になる場面も多いと思います。その点SurrealDBはDBひとつで多機能で柔軟なバックエンドをより簡潔なクエリ言語とフリーのオープンソースソフトウェアで実現できるので、それだけで価値がある気がします。