SQLAlchemyで日付情報の国際化対応(I18N)対応

お久しぶりです。tell-kです。L10NI18N が Localization と Internationalization の略で、10とか18とかは間の文字数だと知って「イカス!!」と思ってから幾数年です。

実は、大分前に書いたgistを眺めてたら、なんだこれ?って全く思い出せなかったのでブログにメモっておきます。

やりたい事

お題としては、DBに入ってる日付情報の海外のTZ対応みたいな感じでしょうか。

やりたい事はしごく簡単で、DBに入ってる日付情報を表示するような、ブログみたいなWebアプリがあったとして日本からアクセスしたら日本時間で表示、海外からアクセスしたら海外の時間で表示したいなんてケースです。

簡単に書くとこんな感じ

仮に"Asia/Tokyo"のタイムゾーンでそもそもDBにデータが入っているとして。

DBの日付情報: 2011-10-20 23:00:00.073015+09:00

  ↓ 「Canda/Atlantic」 からアクセス
 
画面に表示:  2011-10-20 11:00:00.735415-03:00

というのをSQLAlchemy のモデルでやる時に、どうやるのが簡単かという事を考えたのが下記の内容だった(ハズ。。。)

こんな感じにしてみた

#!/usr/bin/env python
#-*- coding:utf8 -*-

from datetime import datetime
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, Date
from sqlalchemy.types import TypeDecorator
from sqlalchemy import DateTime as SdateTime
import time
import pytz

#setting client timezone
#tz = pytz.timezone('Asia/Tokyo')
tz = pytz.timezone('Canada/Atlantic')

class DateTime(TypeDecorator):
    impl = SdateTime
    BASE_TZ = 'Asia/Tokyo'
    def process_bind_param(self, value, engine):
        return value
    def process_result_value(self, value, engine):
        return value.replace(tzinfo=pytz.timezone(self.BASE_TZ)).astimezone(tz)

dsn = 'sqlite:////Users/tell_k/test.db'
engine = create_engine(dsn, convert_unicode=True)
db_session = scoped_session(sessionmaker(autocommit=False,
                                 autoflush=False,
                                 bind=engine))

Base = declarative_base()
Base.query = db_session.query_property()

class Entry(Base):
    __tablename__ = 'entries'
    id = Column(Integer, primary_key=True)
    cdatetime = Column(DateTime, default=datetime.now, nullable=False)

if __name__ == "__main__":
    Base.metadata.drop_all(bind=engine)
    Base.metadata.create_all(bind=engine)

    db_session.add(Entry())
    db_session.commit()

    e = db_session.query(Entry).first()
    print e.cdatetime

SQLAlchemy には TypeDecoratorという便利なものがあってtypeを拡張する事ができる。これを利用して、プロパティを参照した時のフックメソッド「process_result_value」の中で、タイムゾーンをごにょごにょすればおkという感じ。

余談

from sqlalchemy import DateTime as SdateTime

.
.

class DateTime(TypeDecorator):
    impl = SdateTime

.
.

class Entry(Base):
    __tablename__ = 'entries'
    id = Column(Integer, primary_key=True)
    cdatetime = Column(DateTime, default=datetime.now, nullable=False) #カラム定義を変えたくなかった。


なんで上記のようにわざわざSQLAlchemyのDateTimeをSdateTimeとしてインポートして, あらたに「class DateTime」というtypeを作ってるのかというと、カラム定義に利用している「DateTime」を書き換える事なく、日付表示の切り替えを対応したかったから。

いっぱいモデルがあると書き換えて確認するのが面倒だからそうしたような気がする。ただこれは明示的でなく、デフォルトのDateTimeと混同しそうなので、素直に別のtypeを作った方が多分良い。