はじめに
割と久々に Node.js で MySQL に繋ぐプログラムを書いてみたのですが、以前と比べて色々パワーアップしていたので、それらを組み合わせて「現時点のベストプラクティスはこんなんかな?」という感じの検討をしてみました。
多分これが一番スマートだと思います・・・という結論に達したのですが、どうだろうか
Node.js はまだ触り始めたばかりなので、ご意見を頂けると幸いです
ユーティリティ実装
この辺は好みが分かれるところかもしれませんが、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 pool: mysql.Pool
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
})
}
this.pool = mysql.createPool({
host: process.env.RDB_HOST,
user: process.env.RDB_USER,
password: process.env.RDB_PASSWORD,
database: process.env.RDB_NAME
})
}
private sendQuery(dbc: mysql.PoolConnection, query: string, option?: any): Promise<any> {
return new Promise((resolve, reject) => {
dbc.query(query, option, (error, results) => {
if (error) {
reject({ error: error, query: query })
} else {
resolve(results)
}
})
})
}
private async sendQueries(dbc: mysql.PoolConnection, 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 new Promise((resolve, reject) => {
this.pool.getConnection((error, dbc) => {
if (error || !dbc) {
reject(error)
} else {
dbc.config.queryFormat = this.queryFormat
this.sendQuery(dbc, query, option)
.then(result => resolve(result))
.catch(error => reject(error))
.finally(() => dbc.release())
}
})
})
}
queries(queries: Array<{ query: string, option?: any }>): Promise<any> {
return new Promise((resolve, reject) => {
this.pool.getConnection((error, dbc) => {
if (error || !dbc) {
reject(error)
} else {
dbc.config.queryFormat = this.queryFormat
this.sendQueries(dbc, queries)
.then(result => resolve(result))
.catch(error => reject(error))
.finally(() => dbc.release())
}
})
})
}
}
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 の都度 getConnection ~ release している
コネクションプールならそれでも性能的に問題無いという想定
ただし、クエリ実行回数がメチャクチャ多いバッチ処理とかでは流石に若干影響があるかも? → なので、念の為 db.queries で 1 回の getConnection で複数クエリ実行できるようにしてます(これ居るん? と思いつつ念の為)
SQL エラー時は { query: query, error: error } でエラーになった query とエラーオブジェクトを返す ...ようにしたのですが開発時に出てくる dbc.query のエラーケースの大半は SQL の構文エラーなので 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 }
]
まとめて実行したい場合
これが必要なのかはイマイチ判断がつかないのですが念の為
await db.queries([
{ query: sql, option: item1 },
{ query: sql, option: item2 },
{ query: sql, option: item3 },
])
性能検証してみないとなんともですが、これでどの程度処理が早くなるか次第では、queries は廃止した方がユーティリティのコードがシンプルになって良さげかも?
まとめ
async/await いいですね
↧