🦔
SQLAlchemy Enum で文字列値が期待どおりにならずに苦労した話
概要
-
Enum(Color)のように PythonEnumをそのまま SQLAlchemy のEnum型へ渡すと、既定では メンバー名 (EnumMember.name) が列挙体値として採用されます。 - 開発中の Web アプリでは
AccessTypeを小文字値 (value) に統一していましたが、ある列の列挙型が大文字値のまま生成・解釈されていたため、DB が返す小文字値 (public) と整合が取れずLookupErrorが発生しました。
症状
LookupError: 'public' is not among the defined enum values. Enum name: accesstype. Possible values: PRIVATE, PUBLIC, GROUP, LINK
原因分析
-
SQLAlchemyのドキュメントにもある通り、
Enum(SomePythonEnum)をそのまま使うと 列挙値はメンバー名で展開されます。 -
Colorのような Enum を小文字値で定義したとします。class Color(str, Enum): RED = "red" BLUE = "blue" GREEN = "green" -
ところが
Column(Enum(Color))と宣言すると、データベース側にはENUM('RED','BLUE','GREEN')が作成されます。そのため、クエリでフィルタをかけるときにREDが期待される場面でredが返ってきて例外が発生します。from sqlalchemy import Enum # この場合、DB側では ENUM('RED','BLUE','GREEN') が作成されます color = Column(Enum(Color, default=Color.RED))
解決策
Enum 型生成時に values_callable を用いて、Python Enum の .value を列挙値として利用します。
from sqlalchemy import Enum
# この場合、DB側では ENUM('red','blue','green') が作成されます
color = Column(
Enum(
Color,
values_callable=lambda enum_cls: [member.value for member in enum_cls],
name="colortype",
),
default=Color.RED.value,
)
- これにより
Enumが["red", "blue", ...]を列挙値として保持するようになり、DB との整合が取れるようになります。
代替案:Enum の命名スタイルを揃える
-
名前と値の表記を完全に一致させる(例:
class Color(Enum): red = "red")なら、Enum(Color)だけで DB との齟齬は生まれません。# 名前と値の表記を完全に一致させる class Color(str, Enum): red = "red" blue = "blue" green = "green" -
そもそもPythonのドキュメントでは Enumのメンバー名を大文字で書いているのに、SQLAlchemy のドキュメントでは小文字になっており、これが混乱の原因です。実装がどちらのスタイルに寄っているか確認し、プロジェクト内で統一するのが良いと思います。
参考
- SQLAlchemy 公式ドキュメント: sqlalchemy.types.Enum
- Python 公式ドキュメント: enum — 列挙型
Discussion