3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【TypeScript】RDBを手軽にいい感じに扱う @databases

Posted at

@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()関数の中に渡す。COMMITROLLBACKを明示的に呼ぶ必要は無し、ラムダ式が普通に帰れば自動でCOMMITされ、例外が投げられれば自動でROLLBACKされる。

クエリビルダー

クエリビルダーを使うことで、扱えるクエリが単純なものに限られるのと引き換えに、クエリを型安全に書くことができる。

まずは以下のようにCLIを使い、データベースからテーブルのスキーマを取得してTypeScriptの型ファイルを生成する。

npx @databases/mysql-schema-cli \
  --database mysql://test-user:password@localhost:3306/ \
  --schemaName test-db \
  --directory src/__generated__

生成したファイルを使用してクエリビルダーを構築することで、型付きでクエリを呼び出せる。

src/main.ts
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」みたいなログがたくさん出たりした(クエリ自体はちゃんと処理されている)。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?