8
8

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 5 years have passed since last update.

JavaScriptとJSONとMySQLで型の違いに悩まされた

Last updated at Posted at 2016-08-17

以下の技術を組み合わせてWebアプリケーションを作る際に2点問題に直面しました。
解決方法を残しておきます。

技術とバージョン

  • AngularJS 1.4.x
  • Node 4.4.x
    • Express 4.14.x
    • mysql 2.11.x
  • MySQL 5.6.x

クライアントサイドはAngularJSでSPA、サーバーサイドはNode+MySQLでWebAPIという形で実装し、通信時のデータフォーマットはJSON形式です。

発生した問題

  • DB挿入前はBooleanなのに取得後にNumberになる
  • JSON経由すると日時が文字列になる

型の比較

実際の内容の前に、各技術間の型の比較を見ておきます。
基本的には自動的に相互変換されます。

真偽値 日付と時間
JavaScript Boolean Date
JSON true/false string1
MySQL TINYINT(1) DATETIME2

発生した問題

DB挿入前はBooleanなのに取得後にNumberになる

MySQLはTINYINT(1)のカラムに対してtrue/falseの挿入/更新を問題なく実行できます。

INSERT INTO sample (bool_col) VALUES (true);

一方でドライバに依存する部分も多分にあると思いますが、利用したmysqljs/mysqlではそのまま数値として取得されます。
(TINYINT(1)をBOOL型として扱うオプションがあってもいいかもですね)

SELECT bool_col FROM sample; -- 1

結局以下のような流れになります。

AngularJS JSON Node MySQL
Boolean true/false Boolean
TINYINT
Number number Number

JavaScriptでは暗黙の型変換があるため、if文などでは特に困ることはないのですが、
AngularJSの方でチェックボックスなどへのバインドで問題が起きました。
というわけで、SELECTで取得したデータセット内のTINYINT(1)をBoolean型に変換する必要があります。
(JSONでもBoolean型は扱えるため取得時がベスト)

JSON経由すると日時が文字列になる

JSONには日時型が存在しません。
日時はISO-8601拡張形式3の文字列表現に変換されます。
(タイムゾーンは自動でUTCに直される。日本時間であれば、見た目上は9時間前の時間になる。)

AngularJS JSON Node MySQL
Date string String
DATETIME
String string Date

問題となったのは、MySQLにINSERT/UPDATEする際に、表現内のタイムゾーン(UTCであるZ)を無視してMySQLのタイムゾーンで格納されてしまうことでした。
その結果、登録/更新をするたびに、時間がどんどん9時間前にさかのぼってしまいました。
(MySQLのタイムゾーンはJSTにしていました)

日時データの状態遷移
Wed Aug 17 2016 18:00:00 GMT+0900 (JST)

↓ JSON.stringify()

'2016-08-17T09:00:00.000Z'

↓ req.body

'2016-08-17T09:00:00.000Z'

↓ INSERT/UPDATE

2016-08-17 09:00:00

↓ SELECT

Wed Aug 17 2016 09:00:00 GMT+0900 (JST)

↓ JSON.stringify()

'2016-08-17T00:00:00.000Z'

↓ $http.get()

'2016-08-17T00:00:00.000Z'

↓ new Date()

Wed Aug 17 2016 09:00:00 GMT+0900 (JST)

また文字列のままでも概ね問題なく動くのが厄介でしたが、<input type="datetime">などでは、バインドする際に文字列のままだとエラーになります。
AngularJSで文字列のまま日時表示をすると、(当たり前ですが)UTCで表示されます。
(dateフィルターでタイムゾーンを指定すれば大丈夫ですが、そもそもDate型ならタイムゾーンの指定はいらない)

Booleanと違いJSONからデシリアライズするたびにDateに変換する必要があります。

解決方法

BooleanとTINYINT(1)

これは公式にも書かれていますが、typeCastに処理を設定します。
プールまたはコネクション作成時のオプションで指定できます。

var pool = mysql.createPool({
  host    : 'localhost',
  user    : '****',
  password: '****',
  database: '****',
  typeCast: function(field, next) {
    if (field.type === 'TINY' && field.length === 1) {
      return field.string() === '1'; // '1' = true, '0' = false
    }
    return next();
  }
});

Dateとstring

これはサーバーサイドとクライアントサイド双方で変換処理をする必要があります。

サーバーサイド(Node, Express, lodash)

JSON文字列をオブジェクトに変換するには、組み込みオブジェクトであるJSONオブジェクトが持っている、parseメソッドを利用するのが一般的です。
これは実は第2引数を取ることができて、パース後のオブジェクトに対して独自の変換処理を適用することができます。
JSON.parse(text[, reviver])

ドキュメントに書いてありますが、ネストの深いところから再帰的に関数を適用してくれるので、ISO-8601形式の文字列だったらDate型に変換という処理をしてやればいいことになります。

Expressでは、body-parserを設定するときのオプションでreviverを指定できます。

var express = require('express');
var bodyParser = require('body-parser');
var _ = require('lodash');

var app = express();

var ISO_DATE_REGEXP = /^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)$/;
app.use(bodyParser.json({
  reviver: function(key, value) {
    return (_.isString(value) && ISO_DATE_REGEXP.test(value)) ? new Date(value) : value;
  }
}));

クライアントサイド(AngularJS)

AngularJSでも同じことをしてやりたいところですが、残念ながら簡単にはできません。
内部的に$httpが結果を受け取った時に、angular.fromJson()でデシリアライズしてるようなんですが、fromJsonはreviverの指定はできない上に、それっぽいオプションも見当たりません。
代わりに、$httpProviderのtransformResponseに同様の処理をpushすることで追加します。

var ISO_DATE_REGEXP = /^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)$/;

function convertDate(obj) {
  angular.forEach(obj, function(value, key) {
    if (angular.isString(value) && ISO_DATE_REGEXP.test(value)) {
      obj[key] = new Date(value);
    } else if (angular.isArray(value) || angular.isObject(value)) {
      convertDate(value);
    }
  });
}

angular.module('****').config(function($httpProvider) {
  $httpProvider.defaults.transformResponse.push(function(data) {
    convertDate(data);
    return data;
  });
});

まとめ

こういう時にきちんと型指定できる言語だと安心感がありますね。
JavaScriptは型にゆるい言語ですが、うまいことはめられました。
型には気をつけよう!


  1. JSON.stringify()を利用するとISO-8601の文字列表現となる。

  2. DATE型やTAIMESTAMP型、TIME型など複数ある。

  3. 基本形式と拡張形式

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?