SQLModelでcreated_at とupdated_atカラムを自動で更新する【Python3】
はじめに
せっかくORMを使っているのであれば、いちいち手動で設定しなくても、insertやupdateしたタイミングで、created_atやupdated_atカラムをいい感じにセットして欲しいですよね。
Laravel(PHP)で使われているEloquentなどではデフォルトの機能としてありますが、SQLModelではドキュメントに記述を見つけられず少し悩みました。また悩むのも嫌なので備忘録として。
環境
M1 MBA、macOS 13で、HomebrewでインストールしたPython3.10.8を使用しています。
PC: MacBook Air M1 2020
OS: macOS Ventura 13.0
Python: 3.10.8
sqlmodel: 0.0.8
SQLAlchemy: 1.4.41
pydantic: 1.10.2
SQLModelとは
FastAPIの製作者が制作しているPython3用のORM。
ORMとはいっても、PydanticとSQLAlchemyのラッパーのようなライブラリ。
本来別々に書かないといけないPydanticとSQLAlchemyの定義を一発で書くことができ、便利そうなので使ってみることにしました。
まだバージョンが0.0.8
であるにも関わらず、Githubのスター数が8.8kあったりします。
(コアの機能はPydanticやSQLAlchemy側が持っているので、あまり変更がいらないのかも。。。?)
Advanced User Guideがまだなかったりするので、発展途上感はあります。
裏側でSQLAlchemyを使っているので、知っているとわかりやすい部分があるかもしれません。
やり方
本題の通り、created_atとupdated_atを自動で
まずは実際SQLModel定義部分のコードは以下のようになる。
from datetime import datetime
from sqlmodel import Field, SQLModel
from typing import Optional # python3.9以前のみ
class User(SQLModel, table=True):
# 主キーを設定
id: int | None = Field(default=None, primary_key=True)
# python3.9までは以下
# id: Optional[int] = Field(default=None, primary_key=True)
# 通常のカラム定義
name: str
# ...
# ...
# created_at
created_at: datetime = Field(default_factory=datetime.now, nullable=False)
# updated_at
updated_at: datetime = Field(
default_factory=datetime.now, nullable=False,
sa_column_kwargs={'onupdate': datetime.now})
それぞれ以下で説明します。
created_at
初期値の設定: default
ではなく、SQLModelのdefault_factory
にdatetime.now
関数を設定してやります。
created_at: datetime = Field(default_factory=datetime.now, nullable=False)
updated_at
自動更新: まず、初期値はcreated_at
と同様に指定します。
ドキュメントで見つけられなかったのですが、SQLModelのField関数はsa_column_kwargs
というキーワード引数に辞書を渡して、SQLAlchemyのColumnに渡す引数を指定できます。
これを使用して、SQLAlchmeyの機能でupdated_at
を更新します。
以下のように、キーワード引数名の文字列と、渡す値を辞書型で指定します。(今回はonupdate
と、datetime.now
関数とを指定します。
updated_at: datetime = Field(
default_factory=datetime.now, nullable=False,
sa_column_kwargs={'onupdate': datetime.now})
動作確認
SQLiteを使用してテストします。
テストコード
from time import sleep
from datetime import datetime
from sqlmodel import Field, SQLModel, create_engine, Session, select
# モデルを定義
class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
created_at: datetime = Field(default_factory=datetime.now, nullable=False)
updated_at: datetime = Field(
default_factory=datetime.now, nullable=False,
sa_column_kwargs={'onupdate': datetime.now})
# DBへの接続を定義
sqlite_file_name = "database.sqlite"
sqlite_url = f"sqlite:///{sqlite_file_name}"
engine = create_engine(sqlite_url, echo=True)
# テーブルを作成
SQLModel.metadata.create_all(engine)
# Insert 1
with Session(engine) as session:
user_1 = User(name='Taro')
user_2 = User(name='Hanako')
session.add(user_1)
session.add(user_2)
session.commit()
# 3秒Sleep
sleep(3)
with Session(engine) as session:
user_3 = User(name='Jiro')
session.add(user_3)
session.commit()
# 3秒Sleep
sleep(3)
# Update
with Session(engine) as session:
stmt = select(User)
user = session.exec(stmt).first()
if user is None:
raise RuntimeError('user not found')
user.name = 'Takashi'
session.commit()
with Session(engine) as session:
stmt = select(User)
users = session.exec(stmt).all()
print(users)
詰まったところ
SQLAlchemyのColumnとSQLModelのFieldは微妙に違うので、少し注意が必要でした。
default_factory
を使用する
SQLAlchemyのColumnのdefault
やonupdate
引数は公式ドキュメントで以下のように言われている通り、Pythonの関数を渡すことができます。
The Column.default and Column.onupdate keyword arguments also accept Python functions.
https://docs.sqlalchemy.org/en/14/core/defaults.html#python-executed-functions
それに対してSQLModelはdefault
ではなくdefault_factory
が用意されており、defaultには関数を指定できません。 default_factory
を使用しましょう。
同じノリでdefaultに関数を渡すとエラーになりますし、datetime.now
(関数)の代わりにdatetime.now()
(datetimeオブジェクト)を指定すると意味が全く変わってしまうので注意が必要です。
また、sa_column_kargs側でdefault
を設定しても、SQLModel側のdefault
が優先されます。
結論
Pydanticの機能を使いつつ、ストレスフリーにRDBを操作できるようになりました。
いざとなればSQLAlchemyの機能も触れるので、割と自由度も高めです。
PydanticのおかげでVSCodeの補完や型チェックがかなりしっかりかかるようになり、快適になりました。
たとえば、動作テストのコード内の、selectから1件目のレコードを取得する場合のコードでは、以下のような恩恵があります。(ダブって表示されてたり画質が悪いのは気にしないでください)
FastAPIを使用する場合はもちろん、それ以外の場合にも使えそうです。
Discussion