如何将枚举与 SQLAlchemy 和 Alembic 一起使用?

How to use Enum with SQLAlchemy and Alembic?

这是我的 Post 模型:

class Post(Base):
    __tablename__ = 'posts'

    title = db.Column(db.String(120), nullable=False)
    description = db.Column(db.String(2048), nullable=False)

我想向其中添加 Enum status。所以,我创建了一个新的枚举:

import enum

class PostStatus(enum.Enum):
    DRAFT='draft'
    APPROVE='approve'
    PUBLISHED='published'

并为模型添加了一个新字段:

class Post(Base):
    ...
    status = db.Column(db.Enum(PostStatus), nullable=False, default=PostStatus.DRAFT.value, server_default=PostStatus.DRAFT.value)

完成FLASK_APP=server.py flask db migrate后,生成了这样的迁移:

def upgrade():
    op.add_column('posts', sa.Column('status', sa.Enum('DRAFT', 'APPROVE', 'PUBLISHED', name='poststatus'), server_default='draft', nullable=False))

尝试升级数据库后,我得到:

sqlalchemy.exc.ProgrammingError: (psycopg2.ProgrammingError) type "poststatus" does not exist
LINE 1: ALTER TABLE posts ADD COLUMN status poststatus DEFAULT 'draf...
                                            ^
 [SQL: "ALTER TABLE posts ADD COLUMN status poststatus DEFAULT 'draft' NOT NULL"]
  1. 为什么类型 poststatus 不是在数据库级自动创建的?在类似的迁移中。
  2. 如何正确指定server_default选项?我需要 ORM 级默认值和 DB 级默认值,因为我正在更改现有行,因此不应用 ORM 默认值。
  3. 为什么 DB 中的实际值是 'DRAFT'、'APPROVE'、'PUBLISHED' 而不是 draft 等?我认为应该有 ENUM 值,而不是名称。

提前致谢。

来自官方文档:https://docs.python.org/3/library/enum.html#creating-an-enum

import enum

class PostStatus(enum.Enum):
    DRAFT = 0
    APPROVE = 1
    PUBLISHED = 2

据此:

class Post(Base):
    ...
    status = db.Column(db.Integer(), nullable=False, default=PostStatus.DRAFT.value, server_default=PostStatus.DRAFT.value)

1) PostStatus 不是数据库模型,它只是一个包含状态 ID 的 class;

2) 没问题

3) 你不必在数据库中存储状态字符串,你最好使用 ids 代替

我只能回答你问题的第三部分

SQLAlchemy 中 Enum 类型的 documentation 指出:

Above, the string names of each element, e.g. “one”, “two”, “three”, are persisted to the database; the values of the Python Enum, here indicated as integers, are not used; the value of each enum can therefore be any kind of Python object whether or not it is persistable.

因此,SQLAlchemy 设计 Enum 名称,而不是将值持久保存到数据库中。

Why real values in DB are 'DRAFT', 'APPROVE', 'PUBLISHED', but not draft, etc? I supposed there should be ENUM values, not names.

正如 Peter Bašista 已经提到的,SQLAlchemy 在数据库中使用 枚举名称(草稿、批准、发布)。我认为这样做是因为枚举值 ("draft"、"approve"、...) 可以是 Python 中的任意类型,并且不保证它们是唯一的(除非 @unique 被使用)。

但是自从 SQLAlchemy 1.2.3 Enum class 接受一个参数 values_callable 可以用来存储 数据库中的枚举值

    status = db.Column(
        db.Enum(PostStatus, values_callable=lambda obj: [e.value for e in obj]),
        nullable=False,
        default=PostStatus.DRAFT.value,
        server_default=PostStatus.DRAFT.value
    )

Why type poststatus was not created on DB-level automatically? In the similar migration it was.

我认为基本上您遇到了 alembic 的限制:在某些情况下它无法正确处理 PostgreSQL 上的枚举。我怀疑你的主要问题是 Autogenerate doesn't correctly handle postgresql enums #278.

我注意到如果我使用 alembic.op.create_table 类型创建正确,所以我的解决方法基本上是:

enum_type = SQLEnum(PostStatus, values_callable=lambda enum: [e.value for e in enum])
op.create_table(
    '_dummy',
    sa.Column('id', Integer, primary_key=True),
    sa.Column('status', enum_type)
)
op.drop_table('_dummy')
c_status = Column('status', enum_type, nullable=False)
add_column('posts', c_status)

如果您使用的是 PostgreSQL,请使用以下函数示例:

from sqlalchemy.dialects import postgresql
from ... import PostStatus
from alembic import op
import sqlalchemy as sa


def upgrade():
    post_status = postgresql.ENUM(PostStatus, name="status")
    post_status.create(op.get_bind(), checkfirst=True)
    op.add_column('posts', sa.Column('status',  post_status))


def downgrade():
    post_status = postgresql.ENUM(PostStatus, name="status")
    post_status.drop(op.get_bind())