Node + TypeScript で MySQL に接続して Read, Write してみる

## 背景

node + TypeScript で MySQL に繋いで少しだけデータ操作をする場面があったのですが、あまり記事無くて苦戦したので備忘録として残しておきます。

## 実装方針

- O/Rマッパーのような大きいライブラリじゃなくても済む規模だったので [mysql](https://github.com/mysqljs/mysql) を使用して生クエリで対応
- コールバック地獄にならないようにしたい
- しっかりトランザクションはかけておきたい

## バージョン

- node: v14.17
- mysql(ライブラリ): 2.18.1
- MySQL(DB): 5.7

## 実装

### 接続

```ts
import { createPool } from 'mysql';

(async () => {
  const pool = createPool({
    host: process.env.MYSQL_HOST,
    user: process.env.MYSQL_USER,
    password: process.env.MYSQL_PASSWORD,
    database: process.env.MYSQL_DATABASE_NAME,
    debug: ['ComQueryPacket'], // クエリのログだけ出したい時
    // debug: true, // クエリ以外のログ含めて出したい時
  });
})();
```

コネクションプールを貼って接続を使いまわす形にしたいので、 `createPool` で作った（[デフォルトの接続数は10のようです](https://github.com/mysqljs/mysql/tree/v2.18.1#pool-options)）

### Read

```ts
  const list = async (): Promise<any[]> => {
    const sql = `
      select *
      from posts
      where state = ?
    `;

    const exec = promisify(pool.query).bind(pool);
    const rows = (await exec({
      sql: sql,
      values: [0],
    })) as any[];

    return rows
  };

 console.log(await list())
 pool.end() // コネクションプールを明示的に終わらせないと処理が終了しないので注意
```

実際にクエリを投げる処理はコールバックになるので、それを回避するために `Promise` にしてあげた。更にコールバックの形が `(err, value) => ...` な形式かつ特別なハンドリング等もするつもりがなかったので [util.promisify](https://nodejs.org/dist/latest-v8.x/docs/api/util.html#util_util_promisify_original) で書いてコード量を削減した。
ちなみに `pool.query` で[コネクションの確立 -> クエリ実行 -> コネクションの開放までやってくれるみたい](https://github.com/mysqljs/mysql/tree/v2.18.1#pooling-connections)。コネクションを開放しなかったらどんどん増えていってクエリ流せなくなったりとかありそう。
SQL にプレースホルダー付けてあげてその後に配列で順番に入る要素を差し込んであげると[しっかりエスケープしてくれます](https://github.com/mysqljs/mysql/tree/v2.18.1#escaping-query-values)。

### Write

今回は更新で試してみる。

```ts
import { createPool, PoolConnection, QueryOptions, MysqlError } from 'mysql';
// 〜中略〜
  const update = async (): Promise<void> => {
    const sql = `
      update posts
      set state = ?
      where id = ?
    `;

    const conn = await getConnection();
    try {
      await beginTransaction(conn);
      await query(conn, {
        sql: sql,
        values: [1, 0],
      });
      await commit(conn);
    } catch (err) {
      const cErr = await rollback(conn);
      if (cErr) {
        throw cErr;
      } else {
        throw err
      }
    } finally {
      conn.release();
    }
  };

  const getConnection = async (): Promise<PoolConnection> => {
    const getConnFn = promisify(pool.getConnection).bind(pool);
    return await getConnFn();
  };

  const query = (conn: PoolConnection, statement: QueryOptions) => {
    const fn = promisify(conn.query).bind(conn);
    return fn(statement);
  };

  const beginTransaction = (conn: PoolConnection) => {
    return new Promise<void>((resolve, reject) => {
      conn.beginTransaction((err: MysqlError) => {
        if (err) {
          reject(err);
        } else {
          resolve();
        }
      });
    });
  };

  const commit = (conn: PoolConnection) => {
    return new Promise<void>((resolve, reject) => {
      conn.commit((err: MysqlError) => {
        if (err) {
          reject(err);
        } else {
          resolve();
        }
      });
    });
  };

  const rollback = (conn: PoolConnection) => {
    return new Promise((resolve) => {
      conn.rollback((err) => {
        resolve(err);
      });
    });
  };

  try {
    await update()
  } catch (err) {
    console.log(err)
  }
  pool.end() // コネクションプールを明示的に終わらせないと処理が終了しないので注意
```

更新の場合はトランザクションを使用するために、明示的にコネクションを作る必要がある。
トランザクション周りはコールバック地獄なので、 Promise 作って関数として使えるようにしている。このおかげで、再利用性もぐっと高まった。
ロールバック時にはエラー起きる可能性もあるので、それも拾って分岐して投げるエラーを変えている。