一番スマートなNode.js+TypeScript+MySQLの使い方

# はじめに

割と久々に Node.js で MySQL に繋ぐプログラムを書いてみたのですが、以前と比べて色々パワーアップしていたので、それらを組み合わせて「現時点のベストプラクティスはこんなんかな？」という感じの検討をしてみました。

多分これが一番スマートだと思います・・・という結論に達したのですが、どうだろうか:thinking:

Node.js はまだ触り始めたばかりなので、ご意見を頂けると幸いです :bow:

# ユーティリティ実装

この辺は好みが分かれるところかもしれませんが、DBアクセス用のユーティリティを作ります。

デザインパターン的にコレがベストかは少し自信がありませんが、Node.js だと流行りのパッケージはちょくちょく変わる（ex: mysql が mysql2 になったり、redisが ioredis になったり...etc）ので、自前コードで一枚噛ませて（ラップして）おいた方が良いと思います。（モノによりけりかもしれませんがインフラストラクチャ関連は特に）

## 関連パッケージ

```
npm install --save dotenv
npm install --save mysql2
npm install --save-dev @types/mysql
```

## DBユーティリティ実装

```db.ts
import mysql from 'mysql2'

require('dotenv').config()

class DatabaseUtility {
    private queryFormat: any

    constructor() {
        this.queryFormat = (query: string, values: Array<string>) => {
            if (!values) return query
            return query.replace(/\:(\w+)/g, (txt, key) => {
                return values.hasOwnProperty(key) ? mysql.escape(values[key]) : txt
            })
        }
    }

    private connect(callback: (dbc: mysql.Connection) => Promise<any>): Promise<any> {
        return new Promise((resolve, reject) => {
            const dbc = mysql.createConnection({
                host: process.env.RDB_HOST,
                user: process.env.RDB_USER,
                password: process.env.RDB_PASSWORD,
                database: process.env.RDB_NAME
            })
            dbc.connect((error) => {
                if (error) {
                    reject(error)
                } else {
                    dbc.config.queryFormat = this.queryFormat
                    callback(dbc)
                        .then(result => resolve(result))
                        .catch(error => reject(error))
                        .finally(() => dbc.end())
                }
            })
        })
    }

    private sendQuery(dbc: mysql.Connection, query: string, option?: any): Promise<any> {
        return new Promise((resolve, reject) => {
            dbc.query(query, option, (error, results) => {
                if (error) {
                    reject(new Error(`SQL error: $query`))
                } else {
                    resolve(results)
                }
            })
        })
    }

    private async sendQueries(dbc: mysql.Connection, queries: Array<{ query: string, option?: any }>) {
        for (var i = 0; i < queries.length; i++) {
            await this.sendQuery(dbc, queries[i].query, queries[i].option)
        }
    }

    query(query: string, option?: any): Promise<any> {
        return this.connect((dbc: mysql.Connection) => this.sendQuery(dbc, query, option))
    }

    queries(queries: Array<{ query: string, option?: any }>): Promise<any> {
        return this.connect((dbc: mysql.Connection) => this.sendQueries(dbc, queries))
    }
}

const db = new DatabaseUtility()

export default db
```

> 上記のコードは Public Domain としておきます。

## 要点解説

- 使い方: DB とアクセスするモジュールで `import db from './path/to/db'` をして `db.query`
- `async/await` の対応処理は `db.query` で実装
  - なので、`mysql2/promise` ではなく `mysql2` をそのまま使用
  - 依存パッケージは少なければ少ないほど良いかなと
- コネクション管理とか面倒なので上位レイヤーには意識させない
  - `db.query` の都度 `connect` ~ `end` している
  - `db.queries` で 1 回の `connect` ~ `end` で複数クエリ実行できるようにしている
  - 当初 `ConnectionPool` を使って `db.query` だけで良いかと思ったが、AWS Aurora での使用を想定するとフェイルオーバー時に事故りそうだったので都度接続方式にした
- `dbc.query` のエラーケースの大半は開発時の SQL の構文エラーと考えられる（RDBMSとの通信エラーなどはconnect時に起きる）ので ```reject(new Error(`SQL error: ${query}`))``` としている（別途エラーを記録した方が良いかもしれないが）

# テスト

## 準備

### .env ファイル

```.env
RDB_HOST="localhost"
RDB_USER="root"
RDB_PASSWORD="password"
RDB_NAME="test1"
```

### SQL

```sql
CREATE DATABASE test1;
CREATE TABLE test1.table1 (
    id BIGINT AUTO_INCREMENT,
    name TEXT,
    value INT,
    KEY(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```

### コード

```test.ts
import db from './db'

(async () => {
    const item1 = { name: "hoge", value: 1234 }
    const item2 = { name: "hige", value: 5678 }
    const item3 = { name: "huga", value: 90 }
    const sql = "insert into table1 (name, value) values (:name, :value)"
    await db.query(sql, item1)
    await db.query(sql, item2)
    await db.query(sql, item3)
    return await db.query("select * from table1")
})().then((result) => {
    console.dir(result)
}).catch((error) => {
    console.error(error)
})
```

## 実行結果

正常にクエリ `select * from table1` の実行結果が表示されました。

```bash
 % npx ts-node test
[
  { id: 1, name: 'hoge', value: 1234 },
  { id: 2, name: 'hige', value: 5678 },
  { id: 3, name: 'huga', value: 90 }
]
```

## まとめて実行したい場合

ConnectionPool を使っておけばこれの存在意義はなかったのですが、AWS Aurora でのユースケースを想定して都度 Connect する方式に変更しているので、複数クエリをまとめて実行したい場合、以下のようにすれば高速になります。

```typescript
    await db.queries([
        { query: sql, option: item1 },
        { query: sql, option: item2 },
        { query: sql, option: item3 },
    ])
```

# まとめ

async/await いいですね