この記事を読んでできること
レコード生成時刻(created_at)とレコード更新時刻(updated_at)をSQLAlchemyを使用して作成できる
記述しないこと
- SQLAlchemyとは
- 基本的なモデルの記述方法
使用技術
- Python==3.10.4
- fastapi==0.78.0
- mysqlclient==2.1.0
- pydantic==1.9.1
- SQLAlchemy==1.4.36
- SQLAlchemy-Utils==0.38.2
- uvicorn==0.17.6
今回は DBにMySQLを使用します!
まずはコード例
models/customer.py
from sqlalchemy import DateTime, BigInteger, Column, String, text
from sqlalchemy.sql import func
# Baseクラス作成用にインポート
from sqlalchemy.ext.declarative import declarative_base
# mysqlのutf8は3バイト文字に制限されている為、4バイト文字も処理するutf8mb4をデフォルトで使用する。
class Base(object):
__table_args__ = {"mysql_default_charset": "utf8mb4"}
# Baseクラスを作成
Base = declarative_base(cls=Base)
class Customer(Base):
__tablename__ = "customer"
id = Column(BigInteger, primary_key=True, nullable=False)
last_name = Column(String(20), nullable=False)
first_name = Column(String(20), nullable=False)
email = Column(String(50), nullable=False)
created_at = Column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at = Column(
DateTime(timezone=True),
nullable=False,
server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"),
)
migrationすると以下のような感じでテーブルが作成される
created_at
timezoneを指定する
- Pythonの表中ライブラリdatetimeを使用すれば以下のように日本時間のtimezoneを表示することが可能
- ちなみにdatetimeで生成されるオブジェクトにはという概念がある
- aware(timezoneが有る)
- naive(timezoneが無い)
- ちなみにdatetimeで生成されるオブジェクトにはという概念がある
datetime.py
from datetime import datetime, timedelta, timezone
from dateutil import tz
from zoneinfo import ZoneInfo
# パターン1
now = datetime.now(ZoneInfo("Asia/Tokyo"))
print(now)
# 2022-10-27 13:51:55.293383+09:00
# パターン2
JST = tz.gettz("Asia/Tokyo")
dt = datetime.now(JST)
print(dt)
# 2022-10-27 13:51:55.293613+09:00
# パターン3
JST = timezone(timedelta(hours=+9), "JST")
time = datetime.now(JST)
print(time)
# 2022-10-27 13:51:55.293648+09:00
- パターン3はUTC(協定世界時)とJST(日本時間)の誤差が9時間と知っている前提がないと理解できない(読む人の背景知識に理解が左右される)
- つまり、コメントにて説明を追加することが必須となり、コメント前提のコードってどうなの?となるので暗黙知を許容するコードは書きたくないというのが筆者の考えである
なお。。。
SQLAlchemyでは上記をモデル定義に記述しても正常な挙動が確認できなかった
なので、timezone=True
で指定
SQLAlchemy Datatypes
今の時刻を生成する
以下の公式ドキュメントより参照
SQLAlchemy serverdefault=func.now()
updated_at
- ここで重要なのはいかにして
DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
を追加するかである - 今回はDBにMySQLを使用しているため、
onupdate=datetime.datetime.now
のようなonupdateは使用できない - つまり、このDDLを生成するには、Column.server_defaultパラメータを使用し、ON UPDATE句を含むテキスト句を渡す必要がある
これを知るのに調査の時間がすごくかかったよ。。。。。
この実装を通しての所感
- SQLAlchemy、やれることがたくさんあるけど複雑すぎる
- timezone一つにしても奥が深い
- やっぱり黙って公式ドキュメント参照!
- この実装はなぜいいのか、ダメなのかを意識する
参考
- datetimeオブジェクトのnaiveとaware
- SQLAlchemy default DateTime
- sqlalchemy does not emit server_onupdate ON UPDATE clause with MySQL
- MySQL explicit_defaults_for_timestamp
- SQLAlchemy Datatypes
- SQLAlchemy serverdefault=func.now()