Der beste Weg, um eine Aufzählung in Sqlalchemy zu machen?

72

Ich lese über SQLalchemie und habe folgenden Code gesehen:

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

Sollte ich 'type' zu einem int mit Konstanten in einer Bibliothek machen? Oder sollte ich einfach Typ zu einer Aufzählung machen?

Timmy
quelle

Antworten:

41

SQLAlchemy hat seit 0.6 einen Enum-Typ: http://docs.sqlalchemy.org/en/latest/core/type_basics.html?highlight=enum#sqlalchemy.types.Enum

Obwohl ich die Verwendung nur empfehlen würde, wenn Ihre Datenbank einen nativen Aufzählungstyp hat. Ansonsten würde ich persönlich nur ein int verwenden.

Wolph
quelle
Der zweite Link wird nicht mehr geöffnet
MajesticRa
@MajesticRa: Danke für den Tipp, ich habe den Link entfernt. Es ist sowieso nicht mehr relevant. Jeder hätte vor 0,6 Jahren upgraden sollen;)
Wolph
115

Die aufgezählten Typen von Python sind vom SQLAlchemy Enum-Typ ab SQLAlchemy 1.1 direkt akzeptabel :

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

Beachten Sie, dass oben die Zeichenfolgenwerte "eins", "zwei", "drei" beibehalten werden und nicht die ganzzahligen Werte.

Für ältere Versionen von SQLAlchemy habe ich einen Beitrag geschrieben, der einen eigenen Aufzählungstyp erstellt ( 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()
zzzeek
quelle
Hm, der Link ist noch aktiv, aber der dort veröffentlichte ausführbare Code scheint mit den neuesten Versionen von SQLA (1.0.9) nicht zu funktionieren. Ist dies immer noch Ihre bevorzugte Methode für den Umgang mit Aufzählungen, @zzzeek?
Achiang
Ich habe das Skript gerade unter techspot.zzzeek.org/files/2011/decl_enum.py genau so ausgeführt , wie es gegen den Master ist, und es läuft perfekt.
Zzzeek
Hallo @zzzeek, ​​das habe ich erlebt: gist.github.com/achiang/8d4a6e3f495084d6761c
Achiang
In diesem Beispiel wird eine Metaklasse im Python2-Stil verwendet, die aufgrund ihrer inkompatiblen Metaklassensyntax auf Python 3 portiert werden muss.
Zzzeek
@zzzeek würde es Ihnen etwas ausmachen, auf die Metaklasse hinzuweisen? Ich bin glücklich, dies auf Python 3
Anconia
18

Ich kenne mich in SQLAlchemy nicht wirklich aus, aber dieser Ansatz von Paulo schien mir viel einfacher zu sein.
Ich brauchte keine benutzerfreundlichen Beschreibungen, also habe ich mich dafür entschieden.

Zitat von Paulo (ich hoffe, es macht ihm nichts aus, wenn ich es hier erneut veröffentliche):

Pythons namedtupleSammlung zur Rettung. Wie der Name schon sagt, anamedtuple ist a ein Tupel, bei dem jedes Element einen Namen hat. Wie ein gewöhnliches Tupel sind die Gegenstände unveränderlich. Im Gegensatz zu einem normalen Tupel kann auf den Wert eines Elements über seinen Namen mit der Punktnotation zugegriffen werden.

Hier ist eine Dienstprogrammfunktion zum Erstellen eines namedtuple:

from collections import namedtuple

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

Die *Variable vor den Werten dient zum "Entpacken" der Elemente der Liste, sodass jedes Element als einzelnes Argument an die Funktion übergeben wird.

Um eine zu erstellen namedtuple, rufen Sie einfach die obige Funktion mit den erforderlichen Werten auf:

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

Wir können jetzt das project_versionnamedtuple verwenden, um die Werte des Versionsfelds anzugeben.

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

Das funktioniert gut für mich und ist so viel einfacher als die anderen Lösungen, die ich zuvor gefunden habe.

Dan Abramov
quelle
11

Hinweis: Folgendes ist veraltet. Sie sollten jetzt sqlalchemy.types.Enum verwenden, wie von Wolph empfohlen. Es ist besonders schön, da es PEP-435 seit SQLAlchemy 1.1 entspricht.


Ich mag das Rezept von zzzeek unter http://techspot.zzzeek.org/2011/01/14/the-enum-recipe/ , aber ich habe zwei Dinge geändert:

  • Ich verwende den Python-Namen des EnumSymbols auch als Namen in der Datenbank, anstatt dessen Wert zu verwenden. Ich finde das weniger verwirrend. Ein separater Wert ist weiterhin nützlich, z. B. zum Erstellen von Popup-Menüs in der Benutzeroberfläche. Die Beschreibung kann als längere Version des Werts betrachtet werden, der z. B. für QuickInfos verwendet werden kann.
  • Im Originalrezept ist die Reihenfolge der EnumSymbols beliebig, sowohl wenn Sie sie in Python durchlaufen als auch wenn Sie eine "Reihenfolge nach" in der Datenbank ausführen. Aber oft möchte ich eine bestimmte Reihenfolge haben. Daher habe ich die Reihenfolge so geändert, dass sie alphabetisch ist, wenn Sie die Attribute als Zeichenfolgen oder Tupel festlegen, oder die Reihenfolge, in der die Werte deklariert werden, wenn Sie die Attribute explizit als EnumSymbols festlegen. Dies verwendet denselben Trick wie SQLAlchemy, wenn die Spalten sortiert werden in DeclarativeBase-Klassen.

Beispiele:

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

Hier ist das modifizierte Rezept; Es verwendet die in Python 2.7 verfügbare OrderedDict-Klasse:

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())
Cito
quelle