先日、X を見ていたらタイムラインに surrealDB が流れてきた。 クラウドの受付はまだっポイが、ローカルでインストールして使う分には、いろいろできるらしい。 いつも通り Deno で試してみる。
参考
環境準備 Docker で SurrealDB と Deno 環境をまとめて用意をする。
Dockerfile 1 2 3 4 5 6 7 FROM denoland/deno:1.36 .3 RUN mkdir /usr/src/app WORKDIR /usr/src/app EXPOSE 8080 EXPOSE 40173
docker-compose.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 version: "3" services: app: build: context: . dockerfile: Dockerfile privileged: true command: tail -f /dev/null ports: - "8080:8080" - "40173:40173" volumes: - .:/usr/src/app:cached tty: true env_file: .env db: image: surrealdb/surrealdb:latest command: start --user $SURREALDB_USER --pass $SURREALDB_PASSWORD memory env_file: .env ports: - "8001:8000"
.env 1 2 SURREALDB_USER=[任意] SURREALDB_PASSWORD=[任意]
起動コマンドで、memory
を設定したので、起動都度内容はフラッシュされる。
動作確認 Docker で立てている、deno のコンテナに入って、動作確認していく。
APIなどの解説は、公式ドキュメント を参照する。 が、部分的に古いようで引数の内容が違ったりするので、適宜リポジトリのREADME も見ていく。
接続 公式が、Deno 向けにモジュールを公開されているので、そちらを使う。
app-1.ts 1 2 3 4 5 6 7 8 9 10 11 import "https://deno.land/std@0.201.0/dotenv/load.ts" ;import Surreal from "https://deno.land/x/surrealdb/mod.ts" const db = new Surreal ("http://db:8000/rpc" );await db.signin ({ user : Deno .env .get ("SURREALDB_USER" )!, pass : Deno .env .get ("SURREALDB_PASSWORD" )!, }); await db.close ();
namespace、database 選択 namespace というキーワードが、SurrealDB にはあるのだが、どうやら SurrealDB 独自のものであるらしい。
https://surrealdb.com/docs/introduction/concepts
これらは、個数の制限はないそうだ。
app-2.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 import "https://deno.land/std@0.201.0/dotenv/load.ts" ;import Surreal from "https://deno.land/x/surrealdb/mod.ts" ;const db = new Surreal ("http://db:8000/rpc" );await db.signin ({ user : Deno .env .get ("SURREALDB_USER" )!, pass : Deno .env .get ("SURREALDB_PASSWORD" )!, }); await db.use ({ ns : "ns0" , db : "db0" }); await db.close ();
データ登録 データの登録には、create
と、insert
のAPIがある。 ドキュメントを読む限り create
は単一行、insert
は、複数行対応できる。
app-3.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import "https://deno.land/std@0.201.0/dotenv/load.ts" ;import Surreal from "https://deno.land/x/surrealdb/mod.ts" ;const db = new Surreal ("http://db:8000/rpc" );await db.signin ({ user : Deno .env .get ("SURREALDB_USER" )!, pass : Deno .env .get ("SURREALDB_PASSWORD" )!, }); await db.use ({ ns : "ns0" , db : "db0" });type Food = { id?: string ; name : string ; price : number ; currency : string ; } const result1 = await db.create <Food >('food' ,{ name : 'apple' , price : 100 , currency : 'JPY' }); console .log (result1);
IDは、自動で割り当てされる。 ジェネリクスの割り当てができるが、IDを必須にしておくと、型チェックにパス出来ないのでオプションにしたりする。 ドキュメントにこの辺は書いていなくてすべて必須パラメータで書いてるのでそこは謎。
app-3-1.ts(抜粋) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 type Drink = { id?: string ; name : string ; price : number ; currency : string ; } const result1 = await db.create <Drink >('drink:uuid()' ,{ name : 'water' , price : 100 , currency : 'JPY' }); console .log (result1);const result2 = await db.create <Drink >('drink:ulid()' ,{ name : 'tea' , price : 200 , currency : 'JPY' }); console .log (result2);
ulid()
や uuid()
を書くと、idの形式を変更できる。 この説明は、SDKのドキュメントにはないが、surrealQL のドキュメントにはある 。
ドキュメントに insert があるとは書いたものの、使おうとすると Method not found
を返してエラー。
insertについて issue を探すと出てくる。How to bulk insert from JS client?
query
メソッドを使えばいいらしい。
データ取得 データの取得は、select
を使うか、query
で対応できる。 query は、SQLっぽい SurrealQL 。
app-4.ts(抜粋) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 type Food = { id?: string ; name : string ; price : number ; currency : string ; } const result1 = await db.select <Food >('food' );console .log (result1);const result2 = await db.select <Food >('food:sxbiuuua2bjhaymwg23f' );console .log (result2);const result3 = await db.query <Food []>('SELECT * FROM type::table("food")' );console .log (result3);const result4 = await db.query <Food []>('SELECT * FROM type::table("food") where id = "food:sxbiuuua2bjhaymwg23f"' );console .log (result4);const result5 = await db.query <Food []>('SELECT * FROM type::table("food") where name = "orange"' );console .log (result5);
selectだとデータだけ、 query を使うと応答時間も確認できる。 query だけ使って、応答時間をモニタリングするなんてのもできそう。
データ更新 更新には、update
merge
patch
が使えるが、それぞれ動作が違っている。
app-t.ts(抜粋) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 type Food = { id?: string ; name : string ; price : number ; currency : string ; } const [apple] = await db.create <Food >("food" , { name : "apple" , price : 100 , currency : "JPY" , }); let result :unknown = undefined ;result = await db.select <Food >(apple.id ); console .log (result);await db.update <Partial <Food >>(apple.id , { price : 300 , }); result = await db.select <Food >(apple.id ); console .log (result);const [orange] = await db.create <Food >("food" , { name : "orange" , price : 200 , currency : "JPY" , }); result = await db.select <Food >(orange.id ); console .log (result);await db.merge <Partial <Food >>(orange.id , { price : 300 , }); result = await db.select <Food >(orange.id ); console .log (result);const [banana] = await db.create <Food >("food" , { name : "banana" , price : 300 , currency : "JPY" , }); result = await db.select <Food >(banana.id ); console .log (result);await db.patch (banana.id , [ { op : 'replace' , path : "/price" , value : 600 }, { op : 'remove' , path : "/currency" }, { op : 'add' , path : "/tags" , value : ["fruit" , "yellow" , "sweet" ]} ]); result = await db.select <Food >(banana.id ); console .log (result);
update を不用意に使うと危ない。 JsonPatch というものをしらなかったが、Json ドキュメントを更新するための規格だそうだ。
jsonpatch.com
データの削除 app-6.ts(抜粋) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 type Food = { id?: string ; name : string ; price : number ; currency : string ; } const [apple] = await db.create <Food >("food" , { name : "apple" , price : 100 , currency : "JPY" , }); let result :unknown = undefined ;result = await db.select <Food >("food" ); console .log (result);await db.delete <Partial <Food >>(apple.id );result = await db.select <Food >("food" ); console .log (result);
素直に削除できる。
JOIN 専用のAPIは持っていなさそう。 そしてjoinの概念がそもそも無いようだが、Record link というものがあった。
試してみる。
app-7.ts(抜粋) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 type Place = { id?: string ; name : string ; address : string ; }; type Food = { id?: string ; name : string ; price : number ; currency : string ; productionArea : string ; }; type FoodWithProductionArea = Omit <Food , "productionArea" > & { productionArea : Place };const [place] = await db.create <Place >("place" , { name : "tokyo" , address : "JPY-0000-0000" , }); const [apple] = await db.create <Food >("food" , { name : "apple" , price : 100 , currency : "JPN" , productionArea : place.id , }); const result = await db.query <FoodWithProductionArea []>( ` SELECT *, productionArea.* FROM type::table("food") where id = $id FETCH place; ` , { id : apple.id , } ); console .log (result);
Food => place で関連したレコードをまとめて取得できた。productionArea.*
の記述がポイント。 書かないと次のようになる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const result = await db.query <FoodWithProductionArea []>( ` SELECT * FROM type::table("food") where id = $id FETCH place; ` , { id : apple.id , } ); console .log (result[0 ]);
FETCH place
は、JOINを使用せずに、効率よくデータを取得するための SurrealDB の特徴的な機能だそうだ。 件数が少なかったからだと想定するが、速度的には大きな違いは無かった。
1 対 N も試しておく。
app-7-1.ts(抜粋) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 type Place = { id?: string ; name : string ; address : string ; }; type Food = { id?: string ; name : string ; price : number ; currency : string ; productionArea : string []; }; type FoodWithProductionArea = Omit <Food , "productionArea" > & { productionArea : Place };const [placeA] = await db.create <Place >("place" , { name : "tokyo" , address : "JPN-0000-0000" , }); const [placeB] = await db.create <Place >("place" , { name : "aomori" , address : "JPN-1111-1111" , }); const [apple] = await db.create <Food >("food" , { name : "apple" , price : 100 , currency : "JPY" , productionArea : [placeA.id , placeB.id ], }); const result = await db.query <FoodWithProductionArea []>( ` SELECT *, productionArea.*.* FROM type::table("food") where id = $id FETCH place, human; ` , { id : apple.id , } ); console .log (JSON .stringify (result[0 ], null , 2 ));
ネストした構造も1クエリで取得できた。 3段ネストしたものを試したところ、ちゃんと出ていそうで想定した形にならないというものを見つけた。 フォーラムで聞いてみているので、後で追記したい。
いろいろと試してみた。 すべての機能は触れていない。 事前の定義が無く、使える機能だけ試したが、DEFINE TABLE
で事前定義もできるようだ。
テーブル間でネストした構造や、サンプルを見ると単独のテーブルの中でネスト(name の下に first と last とか)もできる。
面白いので引き続き触ってゆきたい。 ドキュメントを見ていると、接続先として https://cloud.surrealdb.com/rpc
が出てくる。 冒頭の通りやはりまだ公開されていないらしい。 こちらも試したい。
では。
追記:2段階でJOIN 先のものは 1 段階ネストした構造だった。 2段階ネストさせると、 SurrealDB の特徴が際立った。
一旦次のように実装してみた。
app-8.ts(抜粋) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 type Human = { id?: string ; name : string age : number } type Place = { id?: string ; name : string ; address : string ; resident : string ; }; type PlaceWithResident = Omit <Place , "productionArea" > & { resident : Human };type Food = { id?: string ; name : string ; price : number ; currency : string ; productionArea : string []; }; type FoodWithProductionArea = Omit <Food , "productionArea" > & { productionArea : PlaceWithResident [] };const [humanA] = await db.create <Human >("human" , { name : "A-A" , age : 30 , }); const [humanB] = await db.create <Human >("human" , { name : "B-B" , age : 40 , }); const [placeA] = await db.create <Place >("place" , { name : "tokyo" , address : "JPN-0000-0000" , resident : humanA.id , }); const [placeB] = await db.create <Place >("place" , { name : "aomori" , address : "JPN-1111-1111" , resident : humanB.id , }); const [apple] = await db.create <Food >("food" , { name : "apple" , price : 100 , currency : "JPY" , productionArea : [placeA.id , placeB.id ], }); const result = await db.query <FoodWithProductionArea []>( ` SELECT *, productionArea.*.*, productionArea.*.resident.* FROM type::table("food") where id = $id FETCH food, human, place ; ` , { id : apple.id , } ); console .log (JSON .stringify (result[0 ], null , 2 ));
3 段目の内容が変である。この後しばらくいろいろと試して、欲しいものは次のモノだった。
app-8-1.ts(抜粋) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 const result = await db.query <FoodWithProductionArea []>( ` SELECT * FROM type::table("food") where id = $id FETCH productionArea, productionArea.resident ; ` , { id : apple.id , } );
欲しい3段目まで、すべて正しい階層構造で取得できた。 ポイントになるのは、FETCH の指定内容。
1 2 3 SELECT * FROM type::table ("food") where id = $idFETCH productionArea, productionArea.resident
構造に基づいて、順番にたどれるように productionArea, productionArea.resident
を指定する。 すると select は * だけで取得ができてしまう。 これは便利。
SELECT は カラムの選択という意味での横方向、FETCH は取得するレコードの深さ方向の指定と理解するのが良さげ。
今まで親から子を作ることで全体を引き当てたが、JOINをするように子から親も引き当ててみたい。 やり方はこうだった。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 const result = await db.query <[FoodWithProductionArea []]>( `SELECT *, (SELECT * FROM place WHERE id = $id FETCH resident) as productionArea FROM food WHERE $id IN productionArea.id;` , { id : placeA.id , } );
3階層の2階層目から、上の層と下の層を引き当てて結果を返した。 SELECT は、戦闘の結果を後ろのもので上書きする(マージする挙動をする)。
なので、1 つ目の *
と、2 つ目の SELECT * FROM place WHERE id = $id FETCH resident
の重ね合わせが、結果になる。
分割して実行すると次の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 result = await db.query <[FoodWithProductionArea []]>( ` SELECT * FROM food WHERE $id IN productionArea.id; SELECT * FROM place WHERE id = $id FETCH resident; ` , { id : placeA.id , } ); console .log (JSON .stringify (result, null , 2 ));
2つ目の実行結果を as productionArea
して、重ねると最初の結果になる。 結構苦しい気もするが親から子に対しては取得がとても楽なことには変わりがない。
最後に、3段階の一番下から全体を取得してみたかったが、クエリ内での解決は難しかったので見送った。
では。