在Sqlalchemy中进行枚举的最佳方法?


72

我正在阅读有关sqlalchemy的信息,并且看到了以下代码:

employees_table = Table('employees', metadata,
    Column('employee_id', Integer, primary_key=True),
    Column('name', String(50)),
    Column('manager_data', String(50)),
    Column('engineer_info', String(50)),
    Column('type', String(20), nullable=False)
)

employee_mapper = mapper(Employee, employees_table, \
    polymorphic_on=employees_table.c.type, polymorphic_identity='employee')
manager_mapper = mapper(Manager, inherits=employee_mapper, polymorphic_identity='manager')
engineer_mapper = mapper(Engineer, inherits=employee_mapper, polymorphic_identity='engineer')

我应该在库中使用常量将“类型”设置为int吗?还是我应该使枚举成为枚举?

Answers:


41

SQLAlchemy从0.6开始具有Enum类型:http ://docs.sqlalchemy.org/en/latest/core/type_basics.html?highlight=enum#sqlalchemy.types.Enum

尽管我只建议您在数据库具有本机枚举类型的情况下使用它。否则,我个人将只使用int。


第二个链接是不开放了
MajesticRa

@MajesticRa:感谢您的提示,我删除了链接。无论如何,它不再重要了。每个人都应该升级到0.6年前;)
Wolph '10年

115

SQLAlchemy 1.1开始,SQLAlchemy Enum类型直接接受Python的枚举类型:

import enum
from sqlalchemy import Integer, Enum

class MyEnum(enum.Enum):
    one = 1
    two = 2
    three = 3

class MyClass(Base):
    __tablename__ = 'some_table'
    id = Column(Integer, primary_key=True)
    value = Column(Enum(MyEnum))

请注意,上面的字符串值“一个”,“两个”,“三个”是持久的,而不是整数值。

对于旧版本的SQLAlchemy,我写了一篇帖子,创建了自己的枚举类型(http://techspot.zzzeek.org/2011/01/14/the-enum-recipe/

from sqlalchemy.types import SchemaType, TypeDecorator, Enum
from sqlalchemy import __version__
import re

if __version__ < '0.6.5':
    raise NotImplementedError("Version 0.6.5 or higher of SQLAlchemy is required.")

class EnumSymbol(object):
    """Define a fixed symbol tied to a parent class."""

    def __init__(self, cls_, name, value, description):
        self.cls_ = cls_
        self.name = name
        self.value = value
        self.description = description

    def __reduce__(self):
        """Allow unpickling to return the symbol 
        linked to the DeclEnum class."""
        return getattr, (self.cls_, self.name)

    def __iter__(self):
        return iter([self.value, self.description])

    def __repr__(self):
        return "<%s>" % self.name

class EnumMeta(type):
    """Generate new DeclEnum classes."""

    def __init__(cls, classname, bases, dict_):
        cls._reg = reg = cls._reg.copy()
        for k, v in dict_.items():
            if isinstance(v, tuple):
                sym = reg[v[0]] = EnumSymbol(cls, k, *v)
                setattr(cls, k, sym)
        return type.__init__(cls, classname, bases, dict_)

    def __iter__(cls):
        return iter(cls._reg.values())

class DeclEnum(object):
    """Declarative enumeration."""

    __metaclass__ = EnumMeta
    _reg = {}

    @classmethod
    def from_string(cls, value):
        try:
            return cls._reg[value]
        except KeyError:
            raise ValueError(
                    "Invalid value for %r: %r" % 
                    (cls.__name__, value)
                )

    @classmethod
    def values(cls):
        return cls._reg.keys()

    @classmethod
    def db_type(cls):
        return DeclEnumType(cls)

class DeclEnumType(SchemaType, TypeDecorator):
    def __init__(self, enum):
        self.enum = enum
        self.impl = Enum(
                        *enum.values(), 
                        name="ck%s" % re.sub(
                                    '([A-Z])', 
                                    lambda m:"_" + m.group(1).lower(), 
                                    enum.__name__)
                    )

    def _set_table(self, table, column):
        self.impl._set_table(table, column)

    def copy(self):
        return DeclEnumType(self.enum)

    def process_bind_param(self, value, dialect):
        if value is None:
            return None
        return value.value

    def process_result_value(self, value, dialect):
        if value is None:
            return None
        return self.enum.from_string(value.strip())

if __name__ == '__main__':
    from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy import Column, Integer, String, create_engine
    from sqlalchemy.orm import Session

    Base = declarative_base()

    class EmployeeType(DeclEnum):
        part_time = "P", "Part Time"
        full_time = "F", "Full Time"
        contractor = "C", "Contractor"

    class Employee(Base):
        __tablename__ = 'employee'

        id = Column(Integer, primary_key=True)
        name = Column(String(60), nullable=False)
        type = Column(EmployeeType.db_type())

        def __repr__(self):
             return "Employee(%r, %r)" % (self.name, self.type)

    e = create_engine('sqlite://', echo=True)
    Base.metadata.create_all(e)

    sess = Session(e)

    sess.add_all([
        Employee(name='e1', type=EmployeeType.full_time),
        Employee(name='e2', type=EmployeeType.full_time),
        Employee(name='e3', type=EmployeeType.part_time),
        Employee(name='e4', type=EmployeeType.contractor),
        Employee(name='e5', type=EmployeeType.contractor),
    ])
    sess.commit()

    print sess.query(Employee).filter_by(type=EmployeeType.contractor).all()

嗯,链接仍然有效,但是在那里发布的可运行代码似乎不适用于最新版本的SQLA(1.0.9)。@zzzeek仍然是您首选的枚举方法吗?
achiang 2015年

我只是在techspot.zzzeek.org/files/2011/decl_enum.py上运行了脚本,完全是针对master脚本,并且运行得很好。
zzzeek 2015年

@zzzeek大家好,这就是我的经历:gist.github.com/achiang/8d4a6e3f495084d6761c
achiang 2015年

该示例使用python2风格的元类,因此因其不兼容的元类语法而需要移植到Python 3。
zzzeek 2015年

@zzzeek您介意指出元类吗?我很高兴将此移植到Python 3
Anconia '16

18

我对SQLAlchemy并不真正了解,但是Paulo的这种方法对我来说似乎简单得多。
我不需要用户友好的描述,所以我同意了。

引用Paulo(我希望他不介意我在此处重新张贴):

Python的namedtuple集合得以拯救。顾名思义,namedtuple是一个元组,每个项目都有一个名称。像普通的元组一样,项是不可变的。与普通的元组不同,可以使用点符号通过名称访问项目的值。

这是用于创建的实用程序功能namedtuple

from collections import namedtuple

def create_named_tuple(*values):
     return namedtuple('NamedTuple', values)(*values)

*值之前变量是“拆包”的,使每个项目作为一个单独的参数传递给函数列表中的项目。

要创建namedtuple,只需使用所需的值调用上述函数:

>>> project_version = create_named_tuple('alpha', 'beta', 'prod')
NamedTuple(alpha='alpha', beta='beta', prod='prod')

现在,我们可以使用project_versionnamedtuple指定版本字段的值。

class Project(Base):
     ...
     version = Column(Enum(*project_version._asdict().values(), name='projects_version'))
     ...

这对我来说很棒,并且比我以前找到的其他解决方案简单得多。


11

注意:以下内容已过时。根据Wolph的建议,您现在应该使用sqlalchemy.types.Enum。从SQLAlchemy 1.1开始,它符合PEP-435,因此特别好。


我喜欢zzzeek的食谱,网址为http://techspot.zzzeek.org/2011/01/14/the-enum-recipe/,但是我做了两件事:

  • 我也将EnumSymbol的Python名称用作数据库中的名称,而不是使用其值。我认为这不太令人困惑。具有单独的值仍然有用,例如,用于在UI中创建弹出菜单。该描述可以认为是该值的较长版本,可以用于例如工具提示。
  • 在原始配方中,EnumSymbol的顺序是任意的,无论是在Python中迭代它们还是在数据库中执行“ order by”。但通常我想下一个确定的命令。因此,如果将属性设置为字符串或元组,则将顺序更改为字母顺序;如果将属性显式设置为EnumSymbols,则更改了声明值的顺序-这与SQLAlchemy在订购列时使用的技巧相同在DeclarativeBase类中。

例子:

class EmployeeType(DeclEnum):
    # order will be alphabetic: contractor, part_time, full_time
    full_time = "Full Time"
    part_time = "Part Time"
    contractor = "Contractor"

class EmployeeType(DeclEnum):
    # order will be as stated: full_time, part_time, contractor
    full_time = EnumSymbol("Full Time")
    part_time = EnumSymbol("Part Time")
    contractor = EnumSymbol("Contractor")

这是修改后的配方;它使用Python 2.7中提供的OrderedDict类:

import re

from sqlalchemy.types import SchemaType, TypeDecorator, Enum
from sqlalchemy.util import set_creation_order, OrderedDict


class EnumSymbol(object):
    """Define a fixed symbol tied to a parent class."""

    def __init__(self, value, description=None):
        self.value = value
        self.description = description
        set_creation_order(self)

    def bind(self, cls, name):
        """Bind symbol to a parent class."""
        self.cls = cls
        self.name = name
        setattr(cls, name, self)

    def __reduce__(self):
        """Allow unpickling to return the symbol linked to the DeclEnum class."""
        return getattr, (self.cls, self.name)

    def __iter__(self):
        return iter([self.value, self.description])

    def __repr__(self):
        return "<%s>" % self.name


class DeclEnumMeta(type):
    """Generate new DeclEnum classes."""

    def __init__(cls, classname, bases, dict_):
        reg = cls._reg = cls._reg.copy()
        for k in sorted(dict_):
            if k.startswith('__'):
                continue
            v = dict_[k]
            if isinstance(v, basestring):
                v = EnumSymbol(v)
            elif isinstance(v, tuple) and len(v) == 2:
                v = EnumSymbol(*v)
            if isinstance(v, EnumSymbol):
                v.bind(cls, k)
                reg[k] = v
        reg.sort(key=lambda k: reg[k]._creation_order)
        return type.__init__(cls, classname, bases, dict_)

    def __iter__(cls):
        return iter(cls._reg.values())


class DeclEnum(object):
    """Declarative enumeration.

    Attributes can be strings (used as values),
    or tuples (used as value, description) or EnumSymbols.
    If strings or tuples are used, order will be alphabetic,
    otherwise order will be as in the declaration.

    """

    __metaclass__ = DeclEnumMeta
    _reg = OrderedDict()

    @classmethod
    def names(cls):
        return cls._reg.keys()

    @classmethod
    def db_type(cls):
        return DeclEnumType(cls)


class DeclEnumType(SchemaType, TypeDecorator):
    """DeclEnum augmented so that it can persist to the database."""

    def __init__(self, enum):
        self.enum = enum
        self.impl = Enum(*enum.names(), name="ck%s" % re.sub(
            '([A-Z])', lambda m: '_' + m.group(1).lower(), enum.__name__))

    def _set_table(self, table, column):
        self.impl._set_table(table, column)

    def copy(self):
        return DeclEnumType(self.enum)

    def process_bind_param(self, value, dialect):
        if isinstance(value, EnumSymbol):
            value = value.name
        return value

    def process_result_value(self, value, dialect):
        if value is not None:
            return getattr(self.enum, value.strip())
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.