Лучший способ сделать перечисление в Sqlalchemy?
Я читаю о 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')
Должен ли я делать 'type' int, с константами в библиотеке? Или я должен сделать только make type enum?
Ответы
Ответ 1
SQLAlchemy имеет тип Enum с 0.6:
http://docs.sqlalchemy.org/en/latest/core/type_basics.html?highlight=enum#sqlalchemy.types.Enum
Хотя я бы рекомендовал использовать его только в том случае, если ваша база данных имеет собственный тип перечисления. В противном случае я бы просто использовал int.
Ответ 2
Перечислимые типы Python непосредственно приемлемы для типа SQLAlchemy Enum с SQLAlchemy 1.1:
import 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 я написал сообщение, которое создает собственный Enumerated type (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()
Ответ 3
Я не очень хорошо разбираюсь в SQLAlchemy, но этот подход Paulo казался мне намного проще.
Мне не нужны удобные описания, поэтому я пошел с ним.
Цитата Пауло (надеюсь, он не возражает против того, чтобы я его перепродал):
Коллекция Pythons 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_version
namedtuple для указания значений поля версии.
class Project(Base):
...
version = Column(Enum(*project_version._asdict().values(), name='projects_version'))
...
Это отлично работает для меня и намного проще, чем другие решения, которые я ранее нашел.
Ответ 4
Примечание: следующее устарело. Вы должны использовать sqlalchemy.types.Enum сейчас, как рекомендовано Wolph. Это особенно приятно, поскольку он соответствует PEP-435 с SQLAlchemy 1.1.
Мне нравится рецепт zzzeek на http://techspot.zzzeek.org/2011/01/14/the-enum-recipe/, но я изменил две вещи:
- Я использую имя Python для EnumSymbol также как имя в базе данных, вместо того, чтобы использовать его значение. Я думаю, что это менее запутанно. Наличие отдельного значения все еще полезно, например. для создания всплывающих меню в пользовательском интерфейсе. Описание можно рассматривать как более длинную версию значения, которое может быть использовано, например. для всплывающих подсказок.
- В исходном рецепте порядок EnumSymbols является произвольным, как при повторении по ним в 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")
Вот модифицированный рецепт; он использует класс OrderedDict, доступный в Python 2.7:
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())