[JavaScript]Node.jsとDenoとBunでSQLite書き比べ(Pythonもあるよ)
Intro
Cloudflare D1の発表もあり、最近SQLiteが話題になることが増えましたがORMを使うユースケースが多く、ドライバーからSQL文を直接書いてやるやり方について毎回ググるのも面倒になってきたので、書き比べてこれをインポートしてこのメソッドをこう書いて引数と戻り値の型はこんな感じなんだな~みたいなことをまとめてインターネットに放流します。
6種類書きます。やっていきます。
node-sqlite3
// import fs from "fs";
// import sqlite3 from "sqlite3";
// const fs = require("fs");
const sqlite3 = require("sqlite3");
const users = ["a", "b", "c"];
const db = new sqlite3.Database("sqlite.db");
// const sql = fs.readFileSync("schema.sql", "utf8");
db.serialize(() => {
// db.exec(sql);
db.run("CREATE TABLE IF NOT EXISTS users(id INTEGER PRIMARY KEY AUTOINCREMENT, user TEXT)");
for (const user of users) db.run("INSERT INTO users(user) VALUES(?)", user);
db.each("SELECT * FROM users", (_err, row) => {
console.log(row);
});
db.each("SELECT COUNT(*) FROM users WHERE user = ?", "a", (_err, counta) => {
console.log(counta);
});
});
db.close();
$ npm i sqlite3
$ node node-sqlite3-gen.js
おそらく最も歴史があるNode.js用パッケージな気がします。
そのせいか古き良きコールバックを多用しており、普通に動かすと並列実行されてしまうのでsqlite.dbファイルが作られていないエラーが出たり後に書いた処理が先に実行されたりします。
Database#serialize()
のコールバック内に書けばそのような事象を防止できます。
better-sqlite3
// import fs from "fs";
// import Database from "better-sqlite3";
// const fs = require("fs");
const Database = require("better-sqlite3");
const users = ["a", "b", "c"];
const db = new Database("sqlite.db");
// const sql = fs.readFileSync("schema.sql", "utf8");
// db.exec(sql);
db.prepare("CREATE TABLE IF NOT EXISTS users(id INTEGER PRIMARY KEY AUTOINCREMENT, user TEXT)").run();
for (const user of users) db.prepare("INSERT INTO users(user) VALUES(?)").run(user);
for (const row of db.prepare("SELECT * FROM users").iterate()) console.log(row);
for (const counta of db.prepare("SELECT COUNT(*) FROM users WHERE user = ?").iterate("a")) console.log(counta);
db.close();
$ npm i better-sqlite3
$ node better-sqlite3-gen.js
先に書いたnode-sqlite3の使いづらい点を解消してしかも速いNode.js用パッケージです。
ベターを名乗るぐらいなので分かりやすく使いやすいコードを書くことができますが、StatementクラスありきなのでSQL文へバインドする場合はDatabase#prepare()
から始める必要があります。
Statement#all()
を使えばSELECT文で書いたDBを配列で出力してくれますが途中でループを抜け出すような処理をする場合はイテレーターを返すStatement#iterate()
を使ったほうが速いです。
deno-sqlite
import { DB } from "https://deno.land/x/sqlite@v3.5.0/mod.ts";
const users = ["a", "b", "c"];
const db = new DB("sqlite.db");
// const sql = await Deno.readTextFile("schema.sql");
// db.execute(sql);
db.query("CREATE TABLE IF NOT EXISTS users(id INTEGER PRIMARY KEY AUTOINCREMENT, user TEXT)");
for (const user of users) db.query("INSERT INTO users(user) VALUES(?)", [user]);
for (const row of db.queryEntries("SELECT * FROM users")) console.log(row);
for (const counta of db.prepareQuery("SELECT COUNT(*) FROM users WHERE user = ?").iterEntries(["a"])) {
console.log(counta);
}
db.close(true);
$ deno run --allow-read --allow-write deno-sqlite-gen.ts
かんたんDeno用パッケージです。
WebAssemblyを使用しており、OSやCPUが異なっていても簡単に動かせる素晴らしい利点がありますがクラスの名称が他のパッケージと大きく異なっており、DBとPreparedQueryを使います。
またイテレーターでバインドなど行う際PreparedQueryを使うことになるのですが、自動的にfinalizeされないので簡単にcloseできません。
なのでcloseの引数forceにtrueを入れて無理やりfinalizeできるようになっています。うーん。
deno-sqlite3
import { Database } from "https://deno.land/x/sqlite3@v0.5.3/mod.ts";
const users = ["a", "b", "c"];
const db = new Database("sqlite.db");
// const sql = await Deno.readTextFile("schema.sql");
// db.exec(sql);
db.exec("CREATE TABLE IF NOT EXISTS users(id INTEGER PRIMARY KEY AUTOINCREMENT, user TEXT)");
for (const user of users) db.exec("INSERT INTO users(user) VALUES(?)", user);
for (const row of db.prepare("SELECT * FROM users")) console.log(row);
for (const counta of db.prepare("SELECT COUNT(*) FROM users WHERE user = ?").bind("a")) console.log(counta);
db.close();
$ curl -O https://github.com/denodrivers/sqlite3/releases/download/v0.5.3/libsqlite3.so
$ DENO_SQLITE_PATH=libsqlite3.so deno run --unstable --allow-env --allow-ffi deno-sqlite3-gen.ts
先に書いたWebAssembly版Deno用パッケージだとWALモード使えないし書き込み遅い!
色々欠点があるのでモダン? なFFIを使ったDeno用パッケージができました。
様々なパッケージの良いところ取りをしており新し目のTypeScript構文を使うことで非常にシンプルで多様な使い方のできるAPIを使えるところもいいですね。
ただUnstableなFFIを使っているせいかまだv1に到達しておらず--unstable
オプションも必要になります。
またWindowsの場合はsqlite3.dllを別途入れる必要があります。
bun-sqlite
// import fs from "fs";
// import { Database } from "bun:sqlite";
// const fs = require("fs");
const { Database } = require("bun:sqlite");
const users = ["a", "b", "c"];
const db = new Database("sqlite.db");
// const sql = fs.readFileSync("schema.sql", "utf8");
// db.run(sql);
db.run("CREATE TABLE IF NOT EXISTS users(id INTEGER PRIMARY KEY AUTOINCREMENT, user TEXT)");
for (const user of users) db.run("INSERT INTO users(user) VALUES(?)", user);
for (const row of db.query("SELECT * FROM users").all()) console.log(row);
for (const counta of db.query("SELECT COUNT(*) FROM users WHERE user = ?").all("a")) console.log(counta);
db.close();
$ bun run bun-sqlite-gen.js
SQLiteとFFIが組み込まれていて恐ろしい速さを誇るランタイムです(最近はDenoも頑張っているのでそうでもない?)
モジュールですがパッケージと同じようにインポートして使います。
ドキュメントではbetter-sqlite3に基づいていると書かれていますがメソッドが結構異なっており、Deno用パッケージふたつに近い簡単さとシンプルさを感じました。
ただ何分機能が少なくイテレーターなしで配列などの結果を即返すような仕組みなので、速いのはいいけどメモリ管理とか大丈夫なのか不安になります。
とにかくまだオープンになってから数ヶ月しか経っていないですしランタイム自体v1に到達していないので今後に期待したいところです。
python-sqlite3
from contextlib import closing
import sqlite3
users = ["a", "b", "c"]
with closing(sqlite3.connect("file:sqlite.db", uri=True)) as conn:
c = conn.cursor()
# with open("schema.sql", encoding="utf-8") as f:
# sql = f.read()
# c.executescript(sql)
c.execute("CREATE TABLE IF NOT EXISTS users(id INTEGER PRIMARY KEY AUTOINCREMENT, user TEXT)")
for user in users:
c.execute("INSERT INTO users(user) VALUES(?)", (user,))
conn.commit()
for row in c.execute("SELECT * FROM users"):
print(row)
for counta in c.execute("SELECT COUNT(*) FROM users WHERE user = ?", ("a",)):
print(counta)
$ python python-sqlite3-gen.py
以前からよく書くPythonのコードも置いておきます。
PythonはSQLite3パッケージが標準でついてくるので、Raspberry Pi OSでもこのように書けば動きます。
ただcloseの方法が様々あり、(isolation_levelをNoneにしない場合は)DMLのcommitが必要になるので人によっては好き嫌いが分かれるかもしれません。
init
いつものGitpod Ubuntu 20.04で検証しています。DenoはWindowsでも動作確認しています。
$ arch
x86_64
$ node -v
v16.17.0
$ npm -v
8.15.0
$ python -V
Python 3.8.13
$ pip -V
pip 22.2.2 ...
$ curl -fsSL https://deno.land/x/install/install.sh | sh
...
$ export DENO_INSTALL="$HOME/.deno"
$ export PATH="$DENO_INSTALL/bin:$PATH"
$ deno --version
deno 1.25.3 (release, x86_64-unknown-linux-gnu)
v8 10.6.194.5
typescript 4.7.4
$ curl https://bun.sh/install | bash
...
$ export BUN_INSTALL="$HOME/.bun"
$ export PATH="$BUN_INSTALL/bin:$PATH"
$ bun -v
0.1.13
$ touch node-sqlite3-gen.js better-sqlite3-gen.js deno-sqlite-gen.ts deno-sqlite3-gen.ts bun-sqlite-gen.js python-sqlite3-gen.py
$ ls -la
total 0
drwxr-xr-x 2 gitpod gitpod 175 Sep 19 10:00 .
drwxr-xr-x 3 gitpod gitpod 15 Sep 19 10:00 ..
-rw-r--r-- 1 gitpod gitpod 0 Sep 19 10:00 better-sqlite3-gen.js
-rw-r--r-- 1 gitpod gitpod 0 Sep 19 10:00 bun-sqlite-gen.js
-rw-r--r-- 1 gitpod gitpod 0 Sep 19 10:00 deno-sqlite3-gen.ts
-rw-r--r-- 1 gitpod gitpod 0 Sep 19 10:00 deno-sqlite-gen.ts
-rw-r--r-- 1 gitpod gitpod 0 Sep 19 10:00 node-sqlite3-gen.js
-rw-r--r-- 1 gitpod gitpod 0 Sep 19 10:00 python-sqlite3-gen.py
SQL
SQLite CLIから直接SQL文叩く際は"?"に値を入れてください。
CREATE TABLE IF NOT EXISTS users(id INTEGER PRIMARY KEY AUTOINCREMENT, user TEXT);
INSERT INTO users(user) VALUES(?);
SELECT * FROM users;
SELECT COUNT(*) FROM users WHERE user = ?;
Node.jsの表示
{ id: 1, user: 'a' }
{ id: 2, user: 'b' }
{ id: 3, user: 'c' }
...
{ 'COUNT(*)': 1 }
DenoとBunの表示
{ id: 1, user: "a" }
{ id: 2, user: "b" }
{ id: 3, user: "c" }
...
{ "COUNT(*)": 1 }
Pythonの表示
(1, 'a')
(2, 'b')
(3, 'c')
...
(1,)
package.json
CJS版
{
"dependencies": {
"better-sqlite3": "^7.6.2",
"sqlite3": "^5.1.1"
}
"engines": {
"node": "16.x"
}
}
ESM版
{
"dependencies": {
"better-sqlite3": "^7.6.2",
"sqlite3": "^5.1.1"
},
"type": "module",
"engines": {
"node": "16.x"
}
}
Outro
こう見てみるとやっていることはほとんど変わらず、DatabaseとStatementのクラスに関するソースコードやAPIドキュメントを読めば簡単に分かる内容ばかりでした。
ただ微妙に異なる点があり、クラスに関連したコードが増えれば増えるほどソースコードのマイグレーションが難しくなっていきそうに思えます。
CloudflareとDenoとWinterCGなんとかしてくれ~。
Ref
Discussion
deno-sqlite と deno-sqlite3 は名前の揺らぎがあって注意
これと
これは違う。
後者、deno.landやgithubではsqlite3なのにjsrではsqliteになってる。
また後者、
またWindowsの場合はsqlite3.dllを別途入れる必要があります。
と記載あるけどimport { Database } from "jsr:@db/sqlite@0.11";
で確認したら runしたら自動的にダウンロードしてくれたから直接的にdllを意識する必要は無さそう。