[SQLAlchemy] 圧倒的神速理解！！！MySQLにおいてcreated_at, updated_atカラムを作成する方法！！！

# この記事を読んでできること
レコード生成時刻（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すると以下のような感じでテーブルが作成される
<img width="715" alt="スクリーンショット 2022-11-29 11.09.29.png (27.8 kB)" src="https://img.esa.io/uploads/production/attachments/14274/2022/11/29/126296/81f3e6e9-bf20-4034-a1d6-24abc7d04b72.png">

# created_at
## timezoneを指定する
- Pythonの表中ライブラリdatetimeを使用すれば以下のように日本時間のtimezoneを表示することが可能
    - ちなみにdatetimeで生成されるオブジェクトにはという概念がある
        - aware（timezoneが有る）
        - naive（timezoneが無い）
```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
```
- パターン３はUTC（協定世界時）とJST（日本時間）の誤差が９時間と知っている前提がないと理解できない（読む人の背景知識に理解が左右される）
- つまり、コメントにて説明を追加することが必須となり、コメント前提のコードってどうなの？となるので暗黙知を許容するコードは書きたくないというのが筆者の考えである

### なお。。。
SQLAlchemyでは上記をモデル定義に記述しても正常な挙動が確認できなかった
なので、`timezone=True`で指定
[SQLAlchemy Datatypes](https://docs.sqlalchemy.org/en/14/core/type_basics.html)

## 今の時刻を生成する
以下の公式ドキュメントより参照
[SQLAlchemy serverdefault=func.now()](https://docs.sqlalchemy.org/en/14/core/defaults.html)
# updated_at
- ここで重要なのはいかにして`DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`を追加するかである
- 今回はDBにMySQLを使用しているため、`onupdate=datetime.datetime.now`のようなonupdateは使用できない
    - 理由：[TIMESTAMP / DATETIME issues Rendering ON UPDATE CURRENT TIMESTAMP for MySQL / MariaDB’s explicit_defaults_for_timestamp](https://docs.sqlalchemy.org/en/20/dialects/mysql.html#mysql-timestamp-onupdate)
- つまり、このDDLを生成するには、Column.server_defaultパラメータを使用し、ON UPDATE句を含むテキスト句を渡す必要がある
#### これを知るのに調査の時間がすごくかかったよ。。。。。
## この実装を通しての所感
- SQLAlchemy、やれることがたくさんあるけど複雑すぎる
- timezone一つにしても奥が深い
- やっぱり黙って公式ドキュメント参照！
- この実装はなぜいいのか、ダメなのかを意識する
## 参考
- [datetimeオブジェクトのnaiveとaware](https://qiita.com/keishi04hrikzira/items/f77b575e15c9a4fb9365)
- [SQLAlchemy default DateTime](https://stackoverflow.com/questions/13370317/sqlalchemy-default-datetime)
- [sqlalchemy does not emit server_onupdate ON UPDATE clause with MySQL](https://github.com/sqlalchemy/sqlalchemy/issues/3444)
- [MySQL explicit_defaults_for_timestamp](https://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_explicit_defaults_for_timestamp)
- [SQLAlchemy Datatypes](https://docs.sqlalchemy.org/en/14/core/type_basics.html)
- [SQLAlchemy serverdefault=func.now()](https://docs.sqlalchemy.org/en/14/core/defaults.html)

### 今回は特に重要
- [Marking Implicitly Generated Values, timestamps, and Triggered Columns](https://docs.sqlalchemy.org/en/20/core/defaults.html#triggered-columns)
- [TIMESTAMP / DATETIME issues
Rendering ON UPDATE CURRENT TIMESTAMP for MySQL / MariaDB’s explicit_defaults_for_timestamp](https://docs.sqlalchemy.org/en/20/dialects/mysql.html#mysql-timestamp-onupdate)