Java と MySQL で時差を正確に扱う方法。

最近は C#SQL Server を使っているので、JavaMySQL で遊ぶ時間がなくなってしまいました。ちょっと眠っていたネタがあったので忘れる前に書き残しておきます。

日時の国際化

日時の国際化というと、時差の変換が一番イメージしやすいかと思います。例えば twitter の場合、海外のツイートでも、タイムラインでは日本時間で表示されますよね?これを実装する場合、内部的には UTC 時間で保存しておいて、表示する人のタイムゾーンに合わせて時差を変換してやるのが一般的かと思います。

保存先に RDB を使う場合、WHERE で絞り込んだり、ORDER BY でソートする分には、タイムゾーン変換は不要なので、レコードを取得した後にアプリケーション側でタイムゾーン変換をすれば問題ありません。

だけど、GROUP BY を使って日ごとに集計をする場合、UTC をその人のタイムゾーンの日時に変換してから、GROUP BY する必要が出てきます。東京のように UTC との時差が固定(+9時間)の場合は、その分だけ足し引きして GROUP BY すればいいのですが、サマータイムがある国など時差が変動する場合、単純に足し引きすると、変わり目で時差がずれてしまうことが簡単に起きてしまいます。

 

例えば、2012年のアメリカでは 3月11日の2時にサマータイム(Daylight Saving Time)が始まり、時計が3時に進みました。

UTC太平洋標準時
PST (UTC-8)
太平洋夏時間
PDT (UTC-7)
3月11日 08:00 3月11日 00:00  
09:00 01:00  
10:00 02:00 03:00
11:00   04:00
|   |
3月12日 00:00   17:00
|   |
06:00   23:00
07:00   3月12日 00:00
08:00   01:00

このように、3月11日は実質 23 時間ですので(UTCで 3月11日8時から3月12日7時)、この日だけ 23 時間で GROUP BY するようにしないといけません。こういう場合は、DB 側でもタイムゾーン変換をする必要が出てきます。

 

タイムゾーン変換を行う場合、変換の元になるタイムゾーンの情報が必要になります。tz database と呼ばれる有名なデータベースがあり、多くのシステムはこれを元にタイムゾーン変換を行っています。サマータイムや政治的な理由でどこかの国のタイムゾーン情報が変わるたびに、データベースも更新されていて、年に何回も更新されています。現在は ICANN と IANA が管理していて、FTPのリストを見ると 2011 年は a から n まで 14 回更新されていることがわかります。

本題

それで、ここから本題なんですけど、アプリケーションとデータベースの両方でタイムゾーン正確に扱いたい場合、同一のタイムゾーン情報を参照しないと、変換が一致しないケースがでてきます。

そこで今回は JavaMySQLタイムゾーン変換を合わせる方法の一つを紹介したいと思います。試すには MySQLMaven と Git が必要です。Java では Joda Time というライブラリを使います(最近はデファクト?)。今回はうるう秒は扱いません。今年の7月1日に8時59分60秒が挿入されるというホットな話題なので少し調べてみたのですが、話すと長くなってしまうので気が向いたら別エントリで。Joda Time も未サポートみたいですし。

ビルド

まず tzcode と tzdata を IANA からダウンロードして同じディレクトリに展開します(今回は latest)。

wget ftp://ftp.iana.org/tz/tzcode-latest.tar.gz
wget ftp://ftp.iana.org/tz/tzdata-latest.tar.gz
mkdir tz
tar xvzf tzcode-latest.tar.gz -C tz
tar xvzf tzdata-latest.tar.gz -C tz

mysql_tzinfo_to_sql を使って、タイムゾーンデータのSQLファイルを作成します(参考)。

cd tz
make TOPDIR=.. posix_only
cd ..
mkdir output
mysql_tzinfo_to_sql etc/zoneinfo > output/tzinfo.sql

次に Joda Time (2.1) のタイムゾーン情報を変更してビルドします(参考)。 

git clone git://joda-time.git.sourceforge.net/gitroot/joda-time/joda-time
cd joda-time
git checkout -b v2.1_tz tags/v2.1
cp ../tz/* src/main/java/org/joda/time/tz/src
git clean -f
mvn clean package
mv target/*.jar ../output
# git commit -a -m "update tzdata to latest version"
cd ..

これで output ディレクトリに sql と jar ファイルが出来上がり。tzinfo.sqlmysql データベースに流し込めばタイムゾーンが更新されます。joda-time.jar はクラスパスに入れてあげてください。

以上で JavaMySQLタイムゾーン情報が一致し、時差を正確に扱えるようになりました。

テスト

試しに MySQLタイムゾーン変換できるか試してみましょう。

mysql -uroot mysql < output/tzinfo.sql

次にテーブルを作って、データを挿入。

mysql> CREATE TABLE tz (id int AUTO_INCREMENT PRIMARY KEY, dt DATETIME NOT NULL) ENGINE=InnoDB;
mysql> INSERT INTO tz(dt) VALUES ('2012-03-11 08:00'),('2012-03-11 09:00'),('2012-03-11 10:00'),('2012-03-11 11:00'),('2012-03-11 12:00'),('2012-03-11 13:00'),('2012-03-11 14:00'),('2012-03-11 15:00'),('2012-03-11 16:00'),('2012-03-11 17:00'),('2012-03-11 18:00'),('2012-03-11 19:00'),('2012-03-11 20:00'),('2012-03-11 21:00'),('2012-03-11 22:00'),('2012-03-11 23:00'),('2012-03-12 00:00'),('2012-03-12 01:00'),('2012-03-12 02:00'),('2012-03-12 03:00'),('2012-03-12 04:00'),('2012-03-12 05:00'),('2012-03-12 06:00'),('2012-03-12 07:00'),('2012-03-12 08:00');

CONVERT_TZ を使って、タイムゾーン変換してみると。

mysql> SELECT dt UTC, CONVERT_TZ(dt, 'UTC', 'America/Los_Angeles') LA FROM tz;
+---------------------+---------------------+
| UTC                 | LA                  |
+---------------------+---------------------+
| 2012-03-11 08:00:00 | 2012-03-11 00:00:00 |
| 2012-03-11 09:00:00 | 2012-03-11 01:00:00 |
| 2012-03-11 10:00:00 | 2012-03-11 03:00:00 |
| 2012-03-11 11:00:00 | 2012-03-11 04:00:00 |
| 2012-03-11 12:00:00 | 2012-03-11 05:00:00 |
| 2012-03-11 13:00:00 | 2012-03-11 06:00:00 |
| 2012-03-11 14:00:00 | 2012-03-11 07:00:00 |
| 2012-03-11 15:00:00 | 2012-03-11 08:00:00 |
| 2012-03-11 16:00:00 | 2012-03-11 09:00:00 |
| 2012-03-11 17:00:00 | 2012-03-11 10:00:00 |
| 2012-03-11 18:00:00 | 2012-03-11 11:00:00 |
| 2012-03-11 19:00:00 | 2012-03-11 12:00:00 |
| 2012-03-11 20:00:00 | 2012-03-11 13:00:00 |
| 2012-03-11 21:00:00 | 2012-03-11 14:00:00 |
| 2012-03-11 22:00:00 | 2012-03-11 15:00:00 |
| 2012-03-11 23:00:00 | 2012-03-11 16:00:00 |
| 2012-03-12 00:00:00 | 2012-03-11 17:00:00 |
| 2012-03-12 01:00:00 | 2012-03-11 18:00:00 |
| 2012-03-12 02:00:00 | 2012-03-11 19:00:00 |
| 2012-03-12 03:00:00 | 2012-03-11 20:00:00 |
| 2012-03-12 04:00:00 | 2012-03-11 21:00:00 |
| 2012-03-12 05:00:00 | 2012-03-11 22:00:00 |
| 2012-03-12 06:00:00 | 2012-03-11 23:00:00 |
| 2012-03-12 07:00:00 | 2012-03-12 00:00:00 |
| 2012-03-12 08:00:00 | 2012-03-12 01:00:00 |
+---------------------+---------------------+
24 rows in set (0.00 sec)

ちゃんと変換できましたね!後は GROUP BY でもなんでもやっちゃってください。

ついでに

Google Closure Library を使うと JavaScript でもタイムゾーン変換もできちゃいます。詳しいことは id:teppeis による、時を超えた JavaScript で解説されています。goog.i18n.TimeZone 用のタイムゾーンデータを作る必要が出てきますが、残念ながら公式な作成方法が提供されていません!どうやら pytz から作られているようですが、GWT で使われているデータを参考に 自力で tz database から作らないといけないようです。ただ、Closure では 標準時差 (std_offset) を一つしか設定できず、標準時差の変更に対応していません。。。って細かい話は置いておいて、先ほど作成した Joda Time からの方が少しは楽に生成するかと思います。Joda Time がやっていることを真似すれば JavaScript でも上手くタイムゾーンを扱えるので、興味があればソース読んで勉強してみてください。

最後に

この記事では tz database から JavaMySQL 用のタイムゾーン情報を生成する方法を紹介しました。肝心なことは、タイムゾーン情報は頻繁に変更されるので、タイムゾーン変換する時は、元になったタイムゾーン情報が何かを意識するようになってもらいたいなと思います。

(追記)

とは言っても、普通の使い方で影響が出る範囲はほとんどないと思うので、神経質になる必要はないんじゃないかと思います。