AlaSQLを試す

クライアント側だけで使えるデータベースみたいなものってないかなと調べていたら、
良さげなものとして、LokiJSAlaSQLを見つけました。

今回は、AlaSQL を触ってみます。

目次

参考

ALaSQL とは

AlaSQL とは、JavaScript の為のオープンソースの インメモリ SQL データベース。
インメモリではあるが、データのインポート・エクスポートなどで永続化できる。
ブラウザ・Node.js モバイルアプリで動作する。

Node.js で試してみる

導入

以下の手順で導入しました。

1
2
3
4
5
6
7
8
9
10
# 適当なディレクトリで
npm init -y

npm i alasql
npm i typescript@next
npm i @types/node
npm i -D ts-node

# index.tsを作成してから
npx ts-node .\index.ts

動作確認 1

最初の動作確認としてtest1.tsを作成しました。

test1.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const alasql = require("alasql");

interface User {
id: number;
name: string;
}

alasql("CREATE Table users (id typeid AUTOINCREMENT, name string)");
alasql("INSERT INTO users(name) VALUES ('A'),('B'),('C')");
// 全体を取得
const result_all: User[] = alasql("SELECT * FROM users");
// 特定のレコードを取得
const result_select: User[] = alasql("SELECT * FROM users WHERE id=1");
// 取得するレコードを引数で渡す
const result_select_param: User[] = alasql("SELECT * FROM users WHERE id=?", [
2,
]);

console.log(result_all);
console.log(result_select);
console.log(result_select_param);

こちらを実行すると次のようになります。

test1.ts実行結果
1
2
3
4
> npx ts-node test.ts
[ { id: 1, name: 'A' }, { id: 2, name: 'B' }, { id: 3, name: 'C' } ]
[ { id: 1, name: 'A' } ]
[ { id: 2, name: 'B' } ]

メモリ上にテーブルを作成し、レコードを追加、検索しただけなので、何度実行しても結果は同じです。

動作確認 2

続けてtest2.tsで、配列オブジェクトを SQL で検索を試します。

test2.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
const alasql = require("alasql");

interface User {
id: number;
name: string;
}

const data: User[] = [
{
id: 1,
name: "AA",
},
{
id: 2,
name: "BB",
},
];

const result1: User[] = alasql("SELECT * FROM ? WHERE id=2", [data]);
console.log(result1);

//テーブル名が欲しければasで別名を設定
const result2: User[] = alasql("SELECT * FROM ? as data WHERE data.id = ?", [
data,
1,
]);
console.log(result2);

//現在ある配列オブジェクトの中でIDの一番大きな値を取得
let nextId: number = alasql("SELECT max(id) as max FROM ?", [data])[0].max + 1;
console.log(nextId);
//配列にオブジェクトを追加
data.push({ id: nextId, name: "CC" });

const result3: User[] = alasql("SELECT * FROM ? as data", [data]);
console.log(result3);

こちらを実行すると次のようになります。

test2.ts実行結果
1
2
3
4
[ { id: 2, name: 'BB' } ]
[ { id: 1, name: 'AA' } ]
3
[ { id: 1, name: 'AA' }, { id: 2, name: 'BB' }, { id: 3, name: 'CC' } ]

配列オブジェクトを SQL で検索出来ていることがわかります。
自前で、オブジェクトの検索を作るよりもだいぶ簡単且つ汎用的に使えそうです。

インポート・エクスポート 1

続けて、インポートとエクスポートを試します。

test3.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
const alasql = require("alasql");

interface User {
id: number;
name: string;
}

(async () => {
const result_all: User[] = await alasql.promise(
'select * FROM csv("input.csv", {headers:true})'
);
console.log(result_all);

const result_select: User[] = await alasql.promise(
'select * FROM csv("input.csv", {headers:true}) WHERE id=1'
);
console.log(result_select);

const result_select_param: User[] = await alasql.promise(
'select * FROM csv("input.csv", {headers:true}) WHERE id=?',
[2]
);
console.log(result_select_param);

await alasql.promise(
'select * into csv("output.csv", {headers:true, quote:"", separator:","}) FROM ?',
[[{ id: 1, name: "CC" }]]
);
})();

実行のため読み込み対象の input.csv を作成しておきます。

id name
1 AA
2 BB

実行すると次のようになります。

test3.ts実行結果
1
2
3
4
> npx ts-node test3.ts
[ { id: 1, name: 'AA' }, { id: 2, name: 'BB' } ]
[ { id: 1, name: 'AA' } ]
[ { id: 2, name: 'BB' } ]

wiki も調べてみましたが、CSV を読み込みそのままメモリ上で加工、エクスポートという流れがうまく作れませんでした。
select の 3 回それぞれでファイルを参照しています。

インポート・エクスポート 2

AlaSQL の影響するファイルの読み込みではなくバルクインポートを使って CSV データを読み込みをします。
書き出し機能は AlaSQL のものを使います。

CSV データを配列へ変換するために追加でcsv-parseを導入しました。

test4.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
const fs = require("fs");
const parse = require("csv-parse/lib/sync");
const alasql = require("alasql");

interface User {
id: number;
name: string;
}

(async () => {
// fsモジュールで素直にファイルを読み込み
const csvString = await fs.readFileSync("data.csv");

//CSVを解析して、配列オブジェクトへ変換
const csvArr = parse(csvString, {
columns: true,
skip_empty_lines: true,
bom: true,
cast: function (value: string, context: any) {
// 読み込んだ数字が文字列扱いになってしまうため
if (context.column == "id") {
return Number(value);
}
return value;
},
});
console.log(csvArr);

//テーブルを作成し、バルクインサート alasql.tables.[テーブル名].data に配列を渡す
alasql("CREATE TABLE users (id typeid AUTOINCREMENT, name string)");
alasql.tables.users.data = csvArr;

// バルクインサートの結果、IDのオートインクリメントの値が1のままなので、
// テーブルの持っているIDの最大値+1を次のIDの値として使用する
alasql.tables.users.identities.id.value =
alasql("SELECT max(id) as max FROM users")[0].max + 1;
console.log(`Next ID = ${alasql.autoval("users", "id", true)}`);

// データを全件表示
const result_all: User[] = alasql("SELECT * FROM users");
console.log(result_all);

//レコードを追加
alasql("INSERT INTO users(name) VALUES ('CC')");

const result_all_updated: User[] = alasql("SELECT * FROM users");
console.log(result_all_updated);

//書き出し
await alasql.promise(
'SELECT * INTO csv("data.csv", {headers:true, separator:","}) FROM users'
);
})();

実行すると次のようになります。

実行結果
1
2
3
4
5
> npx ts-node .\test4.ts
[ { id: 1, name: 'AA' }, { id: 2, name: 'BB' } ]
Next ID = 3
[ { id: 1, name: 'AA' }, { id: 2, name: 'BB' } ]
[ { id: 1, name: 'AA' }, { id: 2, name: 'BB' }, { id: 3, name: 'CC' } ]

ファイルからインポート・エクスポートでデータのエクスポートができました。

関数にコンパイル

ここまでのソースコードは、SQL をすべてそれぞれにベタ書きしています。
テストは十分にしますが、補完が聞きにくい文字列での SQL の記述は
関数に変換できるので、試します。

test5.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
const alasql = require("alasql");

interface User {
id: number;
name: string;
}

alasql("CREATE TABLE users (id typeid AUTOINCREMENT, name string)");
alasql("INSERT INTO users(name) VALUES ('A'),('B'),('C')");

// SQL内で使用できる関数を定義
alasql.fn.concat = (a, b) => {
return `${a}-${b}`;
};

// 関数を二つ設定
const all: () => User[] = alasql.compile(
"SELECT id, name, concat(id, name) as ct FROM users"
);
const find: (param: (string | number)[]) => User[] = alasql.compile(
"SELECT id, name, concat(id, name) as ct FROM users WHERE id=?"
);

const result_all = all();
const result_select_1 = find([1]);
const result_select_2 = find([2]);

console.log(result_all);
console.log(result_select_1);
console.log(result_select_2);

実行すると次のようになります。

実行結果
1
2
3
4
5
6
7
8
> npx ts-node .\test5.ts
[
{ id: 1, name: 'A', ct: '1-A' },
{ id: 2, name: 'B', ct: '2-B' },
{ id: 3, name: 'C', ct: '3-C' }
]
[ { id: 1, name: 'A', ct: '1-A' } ]
[ { id: 2, name: 'B', ct: '2-B' } ]

ローカルで動作確認してきましたが、とりあえずこのあたりでいいとしましょう。
CSV で試しましたが、XLSX なども使えます。
(最近はめっきり XLSX を開くことも、なかなかなくなってますね。そういえば。)

ブラウザで試してみる

導入

以下の手順で webpack の関連を導入します。

1
2
# 適当なディレクトリで実行
npm install -D webpack webpack-cli webpack-dev-server typescript ts-loader

パッケージが導入できたら、webpack.config.jstsconfig.jsonを続けて作成します。

webpack.config.js
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
const path = require("path");
const outputPath = path.resolve(__dirname, "public");
var IgnorePlugin = require("webpack").IgnorePlugin;

module.exports = {
// バンドルするファイルを指定
entry: "./src/index.ts",
mode: "development",
output: {
// バンドルしてmain.jsとして出力
filename: "bundle.js",
path: outputPath,
},
module: {
rules: [
{
test: /\.ts$/,
use: "ts-loader",
},
],
},
plugins: [
new IgnorePlugin(
/(^fs$|cptable|jszip|xlsx|^es6-promise$|^net$|^tls$|^forever-agent$|^tough-cookie$|cpexcel|^path$|^request$|react-native|^vertx$)/
),
],
resolve: {
extensions: [".ts", ".js"],
},
// webpack-dev-serverを立ち上げた時のドキュメントルートを設定
// ここではdistディレクトリのindex.htmlにアクセスするよう設定してます
devServer: {
contentBase: outputPath,
watchContentBase: true,
port: 3000,
filename: "bundle.js",
hot: true,
},
target: ["web", "es5"],
};
tsconfig.json
1
2
3
4
5
6
7
8
9
{
"compilerOptions": {
"sourceMap": true,
"target": "es5",
"module": "es2015",
"moduleResolution": "node",
"lib": ["es2020", "dom"]
}
}

ブラウザで開くので、ページを用意します。簡単なものです。

public/index.html
1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="/bundle.js"></script>
</head>
<body>
Test
</body>
</html>

実行は次のコマンド。

1
npx webpack serve

(webpack.config.jsは、動作確認はできたけど、不必要な設定も入っている可能性が多分にあるので別途見直したい。)

ブラウザで導入

最初に、作成した test1.ts の内容と同じです。

src/index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const alasql = require("alasql");

interface User {
id: number;
name: string;
}

alasql("CREATE TABLE users (id typeid AUTOINCREMENT, name string)");
alasql("INSERT INTO users(name) VALUES ('A'),('B'),('C')");

const result_all: User[] = alasql("SELECT * FROM users");
const result_select: User[] = alasql("SELECT * FROM users WHERE id=1");
const result_select_param: User[] = alasql("SELECT * FROM users WHERE id=?", [
2,
]);

console.log(result_all);
console.log(result_select);
console.log(result_select_param);

ブラウザで開発ツールを開くと次のようになっています。

実行結果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
(3) [{…}, {…}, {…}]
0: {id: 1, name: "A"}
1: {id: 2, name: "B"}
2: {id: 3, name: "C"}
length: 3
__proto__: Array(0)
[{…}]
0: {id: 1, name: "A"}
length: 1
__proto__: Array(0)
[{…}]
0: {id: 2, name: "B"}
length: 1
__proto__: Array(0)

AlaSQL が、ブラウザでも同様のコードで動作することを確認できました。

ブラウザでデータの永続化

Localstrage をデータベースの書き込み先にできます。
indes.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
const alasql = require("alasql");

interface User {
id: number;
name: string;
}

// localstrageでデータベースを作成
alasql("CREATE localStorage DATABASE IF NOT EXISTS LSDB");
alasql("ATTACH localStorage DATABASE LSDB");

// 使用するデータベースをLSDBに切り替え
// USEをしなかった場合は、以後のテーブルの指定はすべてLSDB.users とする
alasql("USE LSDB");

alasql(
"CREATE TABLE IF NOT EXISTS users (id typeid AUTOINCREMENT, name string)"
);
alasql("INSERT INTO users(name) VALUES ('A'),('B'),('C')");

const result_all: User[] = alasql("SELECT * FROM users");
const result_select: User[] = alasql("SELECT * FROM users WHERE id=1");
const result_select_param: User[] = alasql("SELECT * FROM users WHERE id=?", [
2,
]);

console.log(result_all);
console.log(result_select);
console.log(result_select_param);

ブラウザで開発ツールを開き何度かリロードすると次のようになっています。

実行結果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(30) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
0: {id: 1, name: "A"}
1: {id: 2, name: "B"}
2: {id: 3, name: "C"}
3: {id: 4, name: "A"}
~~省略~~
26: {id: 27, name: "C"}
27: {id: 28, name: "A"}
28: {id: 29, name: "B"}
29: {id: 30, name: "C"}
length: 30
__proto__: Array(0)
[{…}]
0: {id: 1, name: "A"}
length: 1
__proto__: Array(0)
[{…}]
0: {id: 2, name: "B"}
length: 1
__proto__: Array(0)

データが永続化され、リロードのたびにデータが増えていることが確認できました。

初期データだけは、API で提供して以降は Localstrage を参照させるといった用途を感じました。

からインポート・エクスポート

Node.js 環境では CSV ファイルのインポートエクスポートを試しましたが、ブラウザでは<table>タグをデータソースとして使用できます。

bundle.js を読み込む public/index.html を書き換えておきます。

public/index.html
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
<!DOCTYPE html>
<html>
<head> </head>
<body>
Test
<table id="data">
<thead>
<tr>
<th>id</th>
<th>name</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>AA</td>
</tr>
<tr>
<td>2</td>
<td>BB</td>
</tr>
</tbody>
</table>
<script type="text/javascript" src="/bundle.js"></script>
</body>
</html>
src/index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const alasql = require("alasql");

interface User {
id: number;
name: string;
}

const result_all: User[] = alasql(
'select * FROM HTML("#data", {headers:true})'
);
const result_select: User[] = alasql(
'select * FROM HTML("#data", {headers:true}) WHERE id="1"'
);
const result_select_param: User[] = alasql(
'select * FROM HTML("#data", {headers:true}) WHERE id=?',
["2"]
);

console.log(result_all);
console.log(result_select);
console.log(result_select_param);

// HTMLへの書き込みもWIKIには記載があるが動作が確認できなかった。

ブラウザで開発ツールを開くと次のようになっています。

実行結果
1
2
3
4
5
6
7
8
9
10
11
12
13
(2) [{…}, {…}]
0: {id: "1", name: "AA"}
1: {id: "2", name: "BB"}
length: 2
__proto__: Array(0)
[{…}]
0: {id: "1", name: "AA"}
length: 1
__proto__: Array(0)
[{…}]
0: {id: "2", name: "BB"}
length: 1
__proto__: Array(0)

HTML 上のテーブルからデータの取り込みができました。
こちらも、AlaSQL で一旦取り込み配列化。配列からバルクインサートして以降メモリ上だけで操作するのが良いと感じます。
いちいちHTML(~~~を書きたくありませんしね。


今回は、AlaSQL をつかってみました。
SQL で配列オブジェクトの検索ができたりと、バックエンドに投げるほどではない検索処理であれば AlaSQL で実行するのもイイと感じました。

今回は SQL でデータを取り扱うのを目的にしたこともあり AlaSQL を試しましたが Loki.js も触ってみたいところです。

ではでは。