1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

背景

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

実装方針

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

バージョン

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

実装

接続

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のようです

Read

  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 で書いてコード量を削減した。
ちなみに pool.queryコネクションの確立 -> クエリ実行 -> コネクションの開放までやってくれるみたい。コネクションを開放しなかったらどんどん増えていってクエリ流せなくなったりとかありそう。
SQL にプレースホルダー付けてあげてその後に配列で順番に入る要素を差し込んであげるとしっかりエスケープしてくれます

Write

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

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 作って関数として使えるようにしている。このおかげで、再利用性もぐっと高まった。
ロールバック時にはエラー起きる可能性もあるので、それも拾って分岐して投げるエラーを変えている。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?