8
9

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-12-03

はじめに

割と久々に 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 ファイル

RDB_HOST="localhost"
RDB_USER="root"
RDB_PASSWORD="password"
RDB_NAME="test1"

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 の実行結果が表示されました。

 % 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 する方式に変更しているので、複数クエリをまとめて実行したい場合、以下のようにすれば高速になります。

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

まとめ

async/await いいですね

8
9
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
8
9