TypeORM x AuroraDataAPI - TIMEZONE問題への対処
ローカルタイムゾーンがJSTだとしたとき、
DBに書き込まれる時刻が9時間未来になる(JSTのまま書かれてしまう)
DBから読み込まれる時刻が9時間過去になる(JST扱いで読まれてくる)問題への対処です。
前置き
本記事の解決法では、TypeORMの外側で自力で時刻を補正するため、
ライブラリ(TypeORMとAuroraプラグイン)の仕様が変わったり、バグが治った場合は毒になりえます。
そのへんの影響まで自分で管理してやんよシュババってひとむけです。
環境
- Aurora Serverless MySQL 5.6
- time_zone, system_time_zone:
UTC
- time_zone, system_time_zone:
- TypeORM 0.2.24
- typeorm-aurora-data-api-driver 1.1.8
- Node 10
- timezone(TZ環境変数):
Asia/Tokyo
- timezone(TZ環境変数):
上記環境 (TypeORM x AuroraDataAPI) での観測ですが、
DBが違う場合でも同じ症状には同じ対策が適用可能なはずです。
原因
おそらく、 typeorm-aurora-data-api-driver
に関する、以下のバグです。
- Data型を日付時刻Stringに変換して書き込む処理
- TZ環境変数にもとづいて、出力を補正していない
- 結果、書き込まれる時刻がズレる(JSTなら+9時間されてる)
- 配布物でいうと、
typeorm-aurora-data-api-driver.umd.js
のformatDate()
- リポジトリでいうとどこかは、軽く見たけどわからなかった
- 日付時刻StringをDate型に変換して読み出す処理
- TZ環境変数にもとづいて、入力を補正していない
- 結果、読み出される時刻がズレる(JSTなら-9時間されてる)
- ソースで具体的にどこに該当するかは、ざっくりデバッグしたけどわからなかった。
-
query()
中のresult補正かけてるあたりのどこかでやるべき、なのかも。
-
結果、JST環境からの読み書きだけ見ると、同じだけズレるので 書き込み前=読み込み前 となっていますが、
CreatedDateColumn() 等でDB側で生成された時刻を読みだした場合ズレたり、
そもそもDB直接覗くとUTCのくせにJST扱いで記録されてたりと、
いろいろ気持ち悪い現象が発生してしまっていました。
(補足)原因はわかってるけどコントリビュートしたくない
ざっくり調べた結果、上記原因が解消されれば治るは治るはずなんだけど、
以下の背景からコストが高くなりそうなのでしてません。
issueくらいは投稿してあげてもいいかも。手が空いたらやる。
- それがTypeORM由来なのかプラグイン由来なのか正直わかりきらない
- TypeORMのプラグインにコントリビュートするにはTypeORMの知識も必要である
- プラグインの更新が活発でない(8ヶ月前とか)
対策
@Column()
の option である transformer を使い、
書く直前と読んだ直後に自力で時刻を補正します。
Entity インスタンス自体の時刻には影響しないよう気を使っています。
import moment, { Moment } from "moment-timezone";
import { ColumnOptions, EntityMetadata, EntitySchema } from "typeorm";
/**
* Date型のoffsetに基づいて読み書きされているので
*/
const TYPEORM_LOCAL_OFFSET = new Date().getTimezoneOffset();
/**
* DBのタイムゾーンは基本UTCの前提とする。
* 異なる場合は、setCorrectOffset()で任意の補正時間をセットする。
*/
const DB_OFFSET = moment.tz("UTC").utcOffset();
export class OrmOpts {
/**
* TypeORM ごしにDatetime型を書く直前、読んだ直後に、この分数だけ時刻をずらす。
* DB時刻がUTCにも関わらず、JSTで(+09:00のまま)書き込まれたりする問題への対処のため。
*
* 初期値は 0(UTC) - TZ環境変数のoffset(JSTなら540)
*/
static CORRECT_OFFSET = DB_OFFSET - TYPEORM_LOCAL_OFFSET;
static MOMENT: ColumnOptions = {
type: "datetime",
transformer: {
from: (from: Date) => {
if (!from) return from;
const fromM = moment(from).add({
minutes: OrmOpts.CORRECT_OFFSET
});
return fromM;
},
to: (to: Moment) => {
if (!to) return to;
const toD = to
.clone()
.subtract({ minutes: OrmOpts.CORRECT_OFFSET })
.toDate();
return toD;
}
}
};
static DATE: ColumnOptions = {
type: "datetime",
transformer: {
from: (from: Date) => {
if (!from) return from;
const fromM = (OrmOpts.MOMENT.transformer as any).from(from);
return fromM.toDate();
},
to: (to: Date) => {
if (!to) return to;
to = (OrmOpts.MOMENT.transformer as any).to(moment(to));
return to;
}
}
};
}
export class TestEntity extends BaseEntity {
@PrimaryGeneratedColumn("increment") seq: number;
@CreateDateColumn(OrmOpts.DATE) createdAt: Date;
@CreateDateColumn(OrmOpts.MOMENT) createdAtM: Moment;
@Column(OrmOpts.DATE) dateAt: Date;
@Column(OrmOpts.MOMENT) dateAtM: Moment;
}
結果
これで、ローカルのTIMEZONEに関わらず、
正しい時刻(このコードではUTC)でDBに書き込まれるようになります。
読み出すときはローカルTIMEZONEに補正されて読まれるようになります。