@databases
はTypeScriptのRDBクライアントライブラリ。ORMではなく、SQLを直接書く又はクエリビルダーを使う系のライブラリ。Postgres、MySQL、SQLiteなどで使える。
これは私が他のRDBクライアントライブラリに対して「なんでこうしないんだよ」と思っていた設計が全部盛り込まれていて、とても使い心地が良い。
使い方概要
このライブラリにはSQLを直接書く方法とクエリビルダーを使う方法の2種類の使い方がある。2つの長所と短所は以下。
- SQL直書き
- ✅どんなクエリでもかけるのでRDBの機能をフルに活用できる
- ❌TypeScriptの型がつかない
- クエリビルダー
- ✅TypeScriptの型がつく
- ❌扱えるクエリは限られる
SQL直書き
以下がコード例。
import createConnectionPool, { sql } from '@databases/mysql';
const db = createConnectionPool(
'mysql://root@db/codetest',
);
await db.tx(async db => {
await db.query(sql`
INSERT INTO transactions (user_id, amount, description)
SELECT ${request.body.user_id}, ${request.body.amount}, ${request.body.description}
WHERE 0 = (
SELECT IF(SUM(amount) + ${request.body.amount} > 1000, 1, 0)
FROM transactions WHERE user_id = ${request.body.user_id}
)
`);
});
コードの書き方の特徴を以下に示す。
async await
ライブラリ全体で非同期処理にasync
関数が使わている。まあ今時コールバックなんて時代錯誤だから当然だが、意外といろんなライブラリでまだ蔓延ってるからね。
クエリへの値埋め込み
クエリに値を埋め込むのに JavaScript の機能 Tagged Template を用いている。これによって他のライブラリだと
db.query("UPDATE animals SET name = ? WHERE id = ?", [name, id])
のように書かなければならないところ、
db.query(sql`UPDATE animals SET name = ${name} WHERE id = ${id}`)
のようにとてもスマートで読みやすく記述できている。変数を埋め込んだ部分はSQLインジェクションされないようちゃんとエスケープされる。普段あまり使われない Tagged Template の教科書に載せたい活用方法って感じ。
トランザクション
トランザクションを記述するには処理をラムダ式で書いてdb.tx()
関数の中に渡す。COMMIT
やROLLBACK
を明示的に呼ぶ必要は無し、ラムダ式が普通に帰れば自動でCOMMIT
され、例外が投げられれば自動でROLLBACK
される。
クエリビルダー
クエリビルダーを使うことで、扱えるクエリが単純なものに限られるのと引き換えに、クエリを型安全に書くことができる。
まずは以下のようにCLIを使い、データベースからテーブルのスキーマを取得してTypeScriptの型ファイルを生成する。
npx @databases/mysql-schema-cli \
--database mysql://test-user:password@localhost:3306/ \
--schemaName test-db \
--directory src/__generated__
生成したファイルを使用してクエリビルダーを構築することで、型付きでクエリを呼び出せる。
import createConnectionPool from '@databases/mysql';
import tables, {gt} from '@databases/mysql-typed';
import DatabaseSchema, {serializeValue} from './__generated__';
const db = createConnectionPool();
// You can list whatever tables you actually have here:
const {users, posts} = tables<DatabaseSchema>({
serializeValue,
});
await users(db).insert({email: 'me@example.com', favorite_color: 'red'});
const users = users(db).find({favorite_color: 'blue'});
await posts(db).delete({updated_at: gt(new Date(Date.now() - 24 * 60 * 60 * 1000))});
最後の3行のクエリする関数は各引数、戻り値ともにデータベースのスキーマに応じた型が付くのでカラム名を間違えるなどのコーディングミスを減らせる。
注意点
まだマイナーなプロジェクトなので粗が見える部分がある。(私が立てたissueだが)型の宣言がおかしいことがあったり、データベースの切断手続きをちゃんとやってないのか、MySQL側で「Aborted connection」みたいなログがたくさん出たりした(クエリ自体はちゃんと処理されている)。