🦔

SQLAlchemy Enum で文字列値が期待どおりにならずに苦労した話

に公開

概要

  • Enum(Color) のように Python Enum をそのまま 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

原因分析

  1. SQLAlchemyのドキュメントにもある通り、Enum(SomePythonEnum) をそのまま使うと 列挙値はメンバー名で展開されます。

  2. Color のような Enum を小文字値で定義したとします。

    class Color(str, Enum):
        RED = "red"
        BLUE = "blue"
        GREEN = "green"
    
  3. ところが 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 のドキュメントでは小文字になっており、これが混乱の原因です。実装がどちらのスタイルに寄っているか確認し、プロジェクト内で統一するのが良いと思います。

参考

GitHubで編集を提案

Discussion