web-dev-qa-db-fra.com

La meilleure façon de faire l'énumération à Sqlalchemy?

Je lis sur sqlalchemy et j'ai vu le code suivant:

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')

Dois-je faire "taper" un int, avec des constantes dans une bibliothèque? Ou devrais-je simplement faire de type une énumération?

48
Timmy

SQLAlchemy a un type Enum depuis 0.6: http://docs.sqlalchemy.org/en/latest/core/type_basics.html?highlight=enum#sqlalchemy.types.Enum

Bien que je ne recommanderais son utilisation que si votre base de données a un type d'énumération natif. Sinon, j'utiliserais personnellement un int.

32
Wolph

Les types énumérés de Python sont directement acceptables par le type Enum SQLAlchemy à partir de SQLAlchemy 1.1 :

import enum
from sqlalchemy import Integer, Enum

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

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

Notez qu'au-dessus, les valeurs de chaîne "un", "deux", "trois" sont persistantes, pas les valeurs entières.

Pour les anciennes versions de SQLAlchemy, j'ai écrit un article qui crée son propre type Enumerated ( 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 __== '__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):
        __table= '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()
78
zzzeek

Je ne connais pas vraiment SQLAlchemy mais cette approche par Paulo m'a semblé beaucoup plus simple.
Je n'avais pas besoin de descriptions conviviales, donc je suis allé avec.

Citant Paulo (j'espère que ça ne le dérange pas de le republier ici):

La collection namedtuple de Python à la rescousse. Comme son nom l'indique, un namedtuple est un tuple avec chaque élément ayant un nom. Comme un tuple ordinaire, les objets sont immuables. Contrairement à un tuple ordinaire, la valeur d'un élément est accessible via son nom à l'aide de la notation par points.

Voici une fonction utilitaire pour créer un namedtuple:

from collections import namedtuple

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

Le * avant que la variable values ​​soit pour "décompresser" les éléments de la liste afin que chaque élément soit passé comme argument individuel à la fonction.

Pour créer un namedtuple, il suffit d'appeler la fonction ci-dessus avec les valeurs nécessaires:

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

Nous pouvons maintenant utiliser le project_version namedtuple pour spécifier les valeurs du champ de version.

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

Cela fonctionne très bien pour moi et est tellement plus simple que les autres solutions que j'ai trouvées précédemment.

13
Dan Abramov

Remarque: ce qui suit est obsolète. Vous devez maintenant utiliser sqlalchemy.types.Enum, comme recommandé par Wolph. C'est particulièrement agréable car il est conforme au PEP-435 depuis SQLAlchemy 1.1.


J'aime la recette de zzzeek sur http://techspot.zzzeek.org/2011/01/14/the-enum-recipe/ , mais j'ai changé deux choses:

  • J'utilise le nom Python du EnumSymbol également comme nom dans la base de données, au lieu d'utiliser sa valeur. Je pense que c'est moins déroutant. Avoir une valeur distincte est toujours utile, par exemple pour créer menus contextuels dans l'interface utilisateur. La description peut être considérée comme une version plus longue de la valeur qui peut être utilisée par exemple pour les info-bulles.
  • Dans la recette d'origine, l'ordre des EnumSymbols est arbitraire, à la fois lorsque vous les parcourez en Python et aussi lorsque vous effectuez une "commande par" sur la base de données. Mais souvent, je veux avoir un ordre déterminé. J'ai donc changé l'ordre pour qu'il soit alphabétique si vous définissez les attributs comme des chaînes ou des tuples, ou l'ordre dans lequel les valeurs sont déclarées si vous définissez explicitement les attributs comme EnumSymbols - cela utilise la même astuce que SQLAlchemy quand il ordonne les colonnes dans les classes DeclarativeBase.

Exemples:

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")

Voici la recette modifiée; il utilise la classe OrderedDict disponible dans 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())
10
Cito