SQL Alchemyで動的なテーブル名を扱う
こんにちはへたれです。
株式会社アイデミーでエンジニアとして、Lab Bankという化学業界の研究室向けSaaSを開発、運用しています。
はじめに
マルチテナント構成では適切な分離を行うために、同じ種類のデータでもテナント毎にテーブルを分けることがあると思います。
生のSQLを書いてデータのマッピングだけ...というパターンもあると思いますが、せっかくなのでORMの機能をふんだんに使いたいですよね?
本記事ではテナント毎にテーブルが異なる場合、SQL Alchemyでどのように対応するのかについて書きました。
通常の書き方
SQL Alchemyでは通常、以下のようなクラスを作ってデータベースを操作します。
from sqlalchemy.orm import declarative_base
from sqlalchemy import Uuid, String
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(Uuid, primary_key=True)
name: Mapped[str] = mapped_column(String(255))
__tablename__
プロパティで対象のテーブルを指定し、その他のプロパティでテーブルカラムを指定することが可能です。
動的なテーブル名の場合
動的なテーブルを作成するときのコードは以下の通りです。
from sqlalchemy.orm import declarative_base
from sqlalchemy import Uuid, String
Base = declarative_base()
class UserClassFactory:
# テナント毎のクラスを保持
classes: dict[str] = {}
@classmethod
def get_table_class(cls, tenant_name: str):
if(tenant_name in cls.classes):
return cls.classes[theme_name]
camel_tenant_name = ''.join(e.capitalize() for e in tenant_name.split('_'))
# 新規テナントのクラスを作成
new_class = type(
f'{camel_tenant_name}User',
(Base,),
{
# プロパティを辞書形式で渡す
'__tablename__': f'{theme_name}_users',
'id': mapped_column(Uuid, primary_key=True),
'name': mapped_column(String(255), primary_key=True),
)
}
)
cls.classes[theme_name] = new_class
return new_class
tenant_a_class = UserClassFactory.get_table_class('tenant_a')
tenant_b_class = UserClassFactory.get_table_class('tenant_b')
コードを読むとわかるのですが、クラスを動的に作成しています。
UserClassFactory.get_table_class
を呼び出すと引数のテナント名を用いてクラスを生成し、返す形となります。
type()
は引数を用いて新しい型を作る関数です。
第一引数: 型名
第二引数: 基底クラス
第三匹数: 保有するプロパティ
そのため、Baseクラスをしたクラスを動的に生成することが可能となっています。
(Baseクラスを継承したテナント共通の基底クラスを作ればいいじゃないかと思うかもしれませんが、Baseクラスは直接継承しないとダメな制約があるようです...)
おわりに
SQL Alchemyを使って動的なテーブル名に対応する方法についてご紹介しました。
こちらの方法を使えば動的なカラムも扱えると思います。
(保守性が落ちていくのでやりすぎないように!)
ORMで動的にテーブル名を扱うというのは想定していなかったからなのか、なかなかやり方を模索するのが大変でした。
またお世辞にも保守性の高いコードにはならないんだろうな...とは思っています。
(本当は別の方法を探った方が良かったのかもしれません、こういうのがいいよ!というのがあれば是非コメントしてください)
またアイデミーでは一緒に働く仲間を募集中です!
Discussion