はじめに
割と久々に 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ユーティリティ実装
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;
コード
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 いいですね