クライアント側だけで使えるデータベースみたいなものってないかなと調べていたら、 良さげなものとして、LokiJS とAlaSQL を見つけました。
今回は、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 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);const result2 : User [] = alasql ("SELECT * FROM ? as data WHERE data.id = ?" , [ data, 1 , ]); console .log (result2);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 を作成しておきます。
実行すると次のようになります。
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 () => { const csvString = await fs.readFileSync ("data.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 ("CREATE TABLE users (id typeid AUTOINCREMENT, name string)" ); alasql.tables .users .data = csvArr; 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')" );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.js
とtsconfig.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 : { 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" ], }, 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 >
実行は次のコマンド。
(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 ; } alasql ("CREATE localStorage DATABASE IF NOT EXISTS LSDB" );alasql ("ATTACH localStorage DATABASE LSDB" );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);
ブラウザで開発ツールを開くと次のようになっています。
実行結果 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 も触ってみたいところです。
ではでは。