Deno で試すデータベースアクセス(SurrealDB編)

先日、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" }); // < namespace と database を選択

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; // 本来必須なのだが、createの時に自動で付与されるので省略可能にしておかないと型チェックをパス出来ない
name: string;
price: number;
currency: string;
}

const result1 = await db.create<Food>('food',{
name: 'apple',
price: 100,
currency: 'JPY'
});

console.log(result1);
// => [
// {
// currency: "JPY",
// id: "food:64glicnf986t0o3lpmq9",
// name: "apple",
// price: 100
// }
// ]

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);
// => [
// {
// currency: "JPY",
// id: "drink:⟨018a54ab-8bd5-7282-b0be-cefa449b0652⟩",
// name: "water",
// price: 100
// }
// ]

const result2 = await db.create<Drink>('drink:ulid()',{
name: 'tea',
price: 200,
currency: 'JPY'
});
console.log(result2);
// => [
// {
// currency: "JPY",
// id: "drink:01H9AAQ2YR8KGKBK68VPE65B2R",
// name: "tea",
// price: 200
// }
// ]

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;
// 本来必須なのだが、createの時に自動で付与されるので省略可能にしておかないと型チェックをパス出来ない
name: string;
price: number;
currency: string;
}

const result1 = await db.select<Food>('food');
console.log(result1);
// => [
// {
// currency: "JPY",
// id: "food:64glicnf986t0o3lpmq9",
// name: "apple",
// price: 100
// },
// {
// currency: "JPY",
// id: "food:sxbiuuua2bjhaymwg23f",
// name: "orange",
// price: 200
// }
// ]

const result2 = await db.select<Food>('food:sxbiuuua2bjhaymwg23f');
console.log(result2);
// => [
// {
// currency: "JPY",
// id: "food:sxbiuuua2bjhaymwg23f",
// name: "orange",
// price: 200
// }
// ]

const result3 = await db.query<Food[]>('SELECT * FROM type::table("food")');
console.log(result3);
// => [
// {
// time: "97.8µs",
// status: "OK",
// result: [
// {
// currency: "JPY",
// id: "food:64glicnf986t0o3lpmq9",
// name: "apple",
// price: 100
// },
// {
// currency: "JPY",
// id: "food:sxbiuuua2bjhaymwg23f",
// name: "orenge",
// price: 200
// }
// ]
// }
// ]

const result4 = await db.query<Food[]>('SELECT * FROM type::table("food") where id = "food:sxbiuuua2bjhaymwg23f"');
console.log(result4);
// => [
// {
// time: "43.3µs",
// status: "OK",
// result: [
// {
// currency: "JPY",
// id: "food:sxbiuuua2bjhaymwg23f",
// name: "orenge",
// price: 200
// }
// ]
// }
// ]

const result5 = await db.query<Food[]>('SELECT * FROM type::table("food") where name = "orange"');
console.log(result5);
// => [
// {
// time: "36.2µs",
// status: "OK",
// result: [
// {
// currency: "JPY",
// id: "food:sxbiuuua2bjhaymwg23f",
// name: "orenge",
// price: 200
// }
// ]
// }
// ]

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);
// => [
// {
// currency: "JPY",
// id: "food:rx1mrq5zl2xkfxm7sw79",
// name: "apple",
// price: 100
// }
// ]

// update は、フィールドを丸ごと書き換える
// 指定していないフィールドは、空になる
await db.update<Partial<Food>>(apple.id, {
price: 300,
});

result = await db.select<Food>(apple.id);
console.log(result);
// => [
// {
// id: "food:rx1mrq5zl2xkfxm7sw79",
// price: 300
// }
// ]

const [orange] = await db.create<Food>("food", {
name: "orange",
price: 200,
currency: "JPY",
});

result = await db.select<Food>(orange.id);
console.log(result);
// => [
// {
// currency: "JPY",
// id: "food:3epjuwdvfpi3h7ucu8xw",
// name: "orange",
// price: 200
// }
// ]

// merge は、フィールドをマージする
// 指定していないフィールドは、変更されない
await db.merge<Partial<Food>>(orange.id, {
price: 300,
});

result = await db.select<Food>(orange.id);
console.log(result);
// => [
// {
// currency: "JPY",
// id: "food:3epjuwdvfpi3h7ucu8xw",
// name: "orange",
// price: 300
// }
// ]

const [banana] = await db.create<Food>("food", {
name: "banana",
price: 300,
currency: "JPY",
});

result = await db.select<Food>(banana.id);
console.log(result);
// => [
// {
// currency: "JPY",
// id: "food:fmlmp1vwkh2xnvvfsyhx",
// name: "banana",
// price: 300
// }
// ]

// patch は、フィールドをパッチする
// JsonPatch形式で指定する
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);
// => [
// {
// id: "food:fmlmp1vwkh2xnvvfsyhx",
// name: "banana",
// price: 600,
// tags: [ "fruit", "yellow", "sweet" ]
// }
// ]

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);
// => [
// {
// currency: "JPY",
// id: "food:qswsbv3vucf9qigf4hot",
// name: "apple",
// price: 100
// }
// ]

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);
// => {
// time: "279.2µs",
// status: "OK",
// result: [
// {
// currency: "JPY",
// id: "food:vf0ytzcnmtthvox9hec9",
// name: "apple",
// price: 100,
// productionArea: { <= 連携先のデータが取れる
// address: "JPY-0000-0000",
// id: "place:1rzua1xx8npais3vt0p7",
// name: "tokyo"
// }
// }
// ]
// }

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]);
// => {
// time: "194.7µs",
// status: "OK",
// result: [
// {
// currency: "JPY",
// id: "food:qxphy1sfzfzd9vi1tx1p",
// name: "apple",
// price: 100,
// productionArea: "place:aq0rtetyww80rjoifxce"
// }
// ]
// }

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));
// => {
// "time": "402µs",
// "status": "OK",
// "result": [
// {
// "currency": "JPY",
// "id": "food:wh7mdhqnx4997dx9993r",
// "name": "apple",
// "price": 100,
// "productionArea": [
// {
// "address": "JPN-0000-0000",
// "id": "place:hpmvb4lmqybxugz3tqmp",
// "name": "tokyo"
// },
// {
// "address": "JPN-1111-1111",
// "id": "place:0oab6o84pi9u3n706fde",
// "name": "aomori"
// }
// ]
// }
// ]
// }

ネストした構造も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));
// => {
// "time": "306.6µs",
// "status": "OK",
// "result": [
// {
// "currency": "JPY",
// "id": "food:tyc9lqpa9r4iod0chkas",
// "name": "apple",
// "price": 100,
// "productionArea": [
// {
// "address": "JPN-0000-0000",
// "id": "place:del27ensubgyeb5uvevf",
// "name": "tokyo",
// "resident": [
// {
// "age": 30,
// "id": "human:si38c2ry2x7148cz9a7a",
// "name": "A-A"
// },
// { // <= 紐づいている対象が違う
// "age": 40,
// "id": "human:hhmzmib8q7p3cjy6r86k",
// "name": "B-B"
// }
// ]
// },
// {
// "address": "JPN-1111-1111",
// "id": "place:cwt5so7tdv0n1xqfrhrz",
// "name": "aomori",
// "resident": [
// { // <= 紐づいている対象が違う
// "age": 30,
// "id": "human:si38c2ry2x7148cz9a7a",
// "name": "A-A"
// },
// {
// "age": 40,
// "id": "human:hhmzmib8q7p3cjy6r86k",
// "name": "B-B"
// }
// ]
// }
// ]
// }
// ]
// }

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,
}
);
// => {
// "time": "453.4µs",
// "status": "OK",
// "result": [
// {
// "currency": "JPY",
// "id": "food:nq9uygwmkw3p9u2c605q",
// "name": "apple",
// "price": 100,
// "productionArea": [
// {
// "address": "JPN-0000-0000",
// "id": "place:xbkjv5lqvy07njfswh4d",
// "name": "tokyo",
// "resident": {
// "age": 30,
// "id": "human:r0ofbqufptlvnor1h64c",
// "name": "A-A"
// }
// },
// {
// "address": "JPN-1111-1111",
// "id": "place:fhp72pkfmsx8p6q2vqth",
// "name": "aomori",
// "resident": {
// "age": 40,
// "id": "human:msuzb79lv1tktnzk9if4",
// "name": "B-B"
// }
// }
// ]
// }
// ]
// }

欲しい3段目まで、すべて正しい階層構造で取得できた。
ポイントになるのは、FETCH の指定内容。

1
2
3
SELECT *
FROM type::table("food") where id = $id
FETCH 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,
}
);
// => {
// "time": "5.2008ms",
// "status": "OK",
// "result": [
// {
// "currency": "JPY",
// "id": "food:aodftplg67mzp7jbq6ls",
// "name": "apple",
// "price": 100,
// "productionArea": [
// {
// "address": "JPN-0000-0000",
// "id": "place:sihtaagx38h19aj9dzqt",
// "name": "tokyo",
// "resident": {
// "age": 30,
// "id": "human:u7h2r9ml222rmtly9f9a",
// "name": "A-A"
// }
// }
// ]
// }
// ]
// }

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));
// => [
// {
// "time": "4.0042ms",
// "status": "OK",
// "result": [
// {
// "currency": "JPY",
// "id": "food:802a64sffexovaaudekk",
// "name": "apple",
// "price": 100,
// "productionArea": [
// "place:znxjopbc597u8rr9bfo9",
// "place:fxvilzzbrqr4op300l9a"
// ]
// }
// ]
// },
// {
// "time": "1.1988ms",
// "status": "OK",
// "result": [
// {
// "address": "JPN-0000-0000",
// "id": "place:znxjopbc597u8rr9bfo9",
// "name": "tokyo",
// "resident": {
// "age": 30,
// "id": "human:2x1ch2wlkyzk9xb179gl",
// "name": "A-A"
// }
// }
// ]
// }
// ]

2つ目の実行結果を as productionArea して、重ねると最初の結果になる。
結構苦しい気もするが親から子に対しては取得がとても楽なことには変わりがない。

最後に、3段階の一番下から全体を取得してみたかったが、クエリ内での解決は難しかったので見送った。

では。