CLOVER🍀

That was when it all began.

Promise-mysqlで、Node.jsからMySQLにアクセスする

Node.jsからMySQLにアクセスする方法を調べようと思ったのですが、最初に見つけたのがこちら。

mysql

mysql

これが、標準的位置づけなライブラリのようですね。

MySQL 8.0で登場している、Connector/Node.jsはいったん置いておきます
Connector/Node.js

ですが、これだとちょっと辛いので、Promiseを使えるようにしたいなーと思って少し調べたら、そのラッパーがあるようです。

Promise-mysql

Promise-mysql

Bluebirdを使って、Promiseを使えるようにしてあるみたいですね。

Promise-mysql is a wrapper for mysqljs/mysql that wraps function calls with Bluebird promises. Usually this would be done with Bluebird's .promisifyAll() method, but mysqljs/mysql's footprint is different to that of what Bluebird expects.

https://www.npmjs.com/package/promise-mysql

このPromise-mysqlを使って、MySQLにアクセスしてみます。

環境

今回の環境は、こちら。

$ node -v
v8.11.3

$ npm -v
5.6.0

MySQLは、8.0.12を使用しました。

準備

まずは、ライブラリのインストール。

Promise-mysql

$ npm i --save promise-mysql

テストライブラリには、Jestを使用することにしました。

$ npm i --save-dev jest

package.jsonのscript要素。

  "scripts": {
    "test": "jest --detectOpenHandles"
  },

package.jsonで、JestのtestEnvironmentには「node」を指定。

  "jest": {
      "testEnvironment": "node"
  }

MySQL側ですが、テーブル定義はこんな感じで。お題は、書籍とします。

mysql> create table book(isbn varchar(15), title varchar(100), price int, primary key(isbn));

また、現時点のmysqlライブラリではデフォルトの認証形式が変わってしまったため、特になにも指定していないとこんな感じで怒られてしまいます。

ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication protocol requested by server; consider upgrading MySQL client

これを回避するには、my.cnfに「default_authentication_plugin」と「mysql_native_password」と指定するか

default_authentication_plugin = mysql_native_password

ユーザー作成時に以下のように「mysql_native_password」を追加で加えます。

mysql> CREATE USER [username] IDENTIFIED WITH mysql_native_password BY '[password]';

参考)
日々の覚書: MySQL 8.0.4におけるデフォルト認証形式の変更

ここまでで、準備は完了です。

使ってみる

まずは簡単に、

  • MySQLに接続
  • データをINSERT
  • データをSELECT
  • 接続終了

という流れを書いてみます。

require。

const mysql = require('promise-mysql');

作成したコードは、こちら。

test('insert and select', done => {
    let connection;

    mysql.createConnection({
        host: '172.17.0.2',
        port: 3306,
        user: 'kazuhira',
        password: 'password',
        database: 'practice'
    }).then(conn => {
        connection = conn;
        return conn;
    }).then(() => {
        const sql = 'insert into book(isbn, title, price) values(?, ?, ?)';
        const data =['978-4798147406',  '詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド', 3888];

        return connection.query(sql, data);
    }).then(() => {
        const sql = 'select isbn, title, price from book where isbn = ?';
        const data = ['978-4798147406'];

        return connection.query(sql, data);
    }).then(rows => {
        expect(rows).toHaveLength(1);
        expect(rows[0]['isbn']).toEqual('978-4798147406');
        expect(rows[0]['title']).toEqual('詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド');
        expect(rows[0]['price']).toEqual(3888);

        done();
        
        return Promise.resolve();
    }).catch(error => {
        console.log(error);

        done();
        return Promise.resolve();
    }).finally(() => {
        connection.end();
     });
});

mysql#createConnectionで接続、あとは取得したConnectionに対してqueryでSQLを投げたり、endで接続終了、という感じになります。

APIそのものは、元のmysqlライブラリを見ることになるようです。

mysql

mysql

「?」に対して値をバインドさせることも可能なように見えますが、

        const sql = 'insert into book(isbn, title, price) values(?, ?, ?)';
        const data =['978-4798147406',  '詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド', 3888];

        return connection.query(sql, data);

これはどうやらエスケープのようです。
Escaping query values

予約後をエスケープする場合は、こちら。
Escaping query identifiers

async/awaitで書く

先の例は、ふつうにPromiseを使って書きましたが、今度はasync/awaitを使って書き直してみましょう。

Jestでもasync/awaitをサポートしているようなので、こちらの乗ってみます。

Async/Await

作成したのは、こちら。

test('insert and select with async / await', async() => {
    const connection = await mysql.createConnection({
        host: '172.17.0.2',
        port: 3306,
        user: 'kazuhira',
        password: 'password',
        database: 'practice'
    });

    try {
        const insertSql = 'insert into book(isbn, title, price) values(?, ?, ?)';
        const insertData =['978-4774170206',  'MariaDB&MySQL全機能バイブル', 3780];

        await connection.query(insertSql, insertData);

        const selectSql = 'select isbn, title, price from book where isbn = ?';
        const selectData = ['978-4774170206'];

        const rows = await connection.query(selectSql, selectData);

        expect(rows).toHaveLength(1);
        expect(rows[0]['isbn']).toEqual('978-4774170206');
        expect(rows[0]['title']).toEqual('MariaDB&MySQL全機能バイブル');
        expect(rows[0]['price']).toEqual(3780);
    } catch (e) {
        console.log(e);
    } finally {
        await connection.end();
    }
});

データは少し変えましたが、すっきりしたのでは?

トランザクションを使う

最後に、トランザクションを使った例を書いてみましょう。

test('with transaction', async() => {
    const connection = await mysql.createConnection({
        host: '172.17.0.2',
        port: 3306,
        user: 'kazuhira',
        password: 'password',
        database: 'practice'
    });

    try {
        await connection.beginTransaction();

        expect((await connection.query('select price from book where isbn = \'978-4798147406\''))[0]['price']).toEqual(3888);

        await connection.query('update book set price = 5000 where isbn = \'978-4798147406\'');
        expect((await connection.query('select price from book where isbn = \'978-4798147406\''))[0]['price']).toEqual(5000);

        await connection.rollback();

        expect((await connection.query('select price from book where isbn = \'978-4798147406\''))[0]['price']).toEqual(3888);

        await connection.query('update book set price = 4000 where isbn = \'978-4798147406\'');
        expect((await connection.query('select price from book where isbn = \'978-4798147406\''))[0]['price']).toEqual(4000);

        await connection.commit();

        expect((await connection.query('select price from book where isbn = \'978-4798147406\''))[0]['price']).toEqual(4000);
    } catch (e) {
        console.log(e);
    } finally {
        await connection.end();
    }
});

connection#beginTransactionでトランザクションの開始、それからcommit or rollbackという流れです。

こんなところではないでしょうか。