Как использовать SQLAlchemy с атрибутами класса (и свойствами)?

скажем, я делаю игру с элементами в ней (подумайте о Minecraft, CS: GO оружие, LoL и Dota элементы и т.д.). В игре может быть огромное количество одного и того же предмета с незначительными различиями в деталях, такими как состояние/долговечность или количество боеприпасов, оставшихся в элементе:

player1.give_item(Sword(name='Sword', durability=50))
player2.give_item(Sword(name='Sword', durability=80))
player2.give_item(Pistol(name='Pistol', ammo=12))

но поскольку я не хочу каждый раз называть свои мечи и пистолеты (из-за того, что имя всегда одно и то же), и я хочу, чтобы было очень легко создавать новые классы элементов, я решил, что make name атрибут class:

class Item:
    name = 'unnamed item'

теперь я просто подкласс этого:

class Sword(Item):
    name = 'Sword'

    def __init__(self, durability=100):
        self.durability = durability

class Pistol(Item):
    name = 'Pistol'

    def __init__(self, ammo=10):
        self.ammo = ammo

а у нас рабочий класс:

>>> sword = Sword(30)
>>> print(sword.name, sword.durability, sep=', ') 
Sword, 30

но есть ли способ, чтобы использовать эти атрибуты класса (а иногда даже classproperties) С SQLAlchemy так или иначе? Скажем, я хочу сохранить долговечность элемента (атрибут экземпляра) и имя (атрибут класса) с его class_id (свойство класса) в качестве первичного ключа:

class Item:
    name = 'unnamed item'

    @ClassProperty  # see the classproperty link above
    def class_id(cls):
        return cls.__module__ + '.' + cls.__qualname__

class Sword(Item):
    name = 'Sword'

    def __init__(self, durability=100):
        self.durability = durability

в стойкость можно легко сделать с:

class Sword(Item):
    durability = Column(Integer)

а как насчет name атрибут class и class_id свойство класса?

на самом деле у меня гораздо больше дерева наследования, и каждый класс имеет несколько атрибутов/свойств, а также больше атрибутов экземпляра.

обновление: я был неясен в своем посте о таблицах. Я только хочу иметь один таблица для элементов, где class_id используется в качестве первичного ключа. Этот вот как я бы построил таблицу с метаданными:

items = Table('items', metadata,
    Column('steamid', String(21), ForeignKey('players.steamid'), primary_key=True),
    Column('class_id', String(50), primary_key=True),
    Column('name', String(50)),
    Column('other_data', String(100)),  # This is __RARELY__ used for something like durability, so I don't need separate table for everything
)

3 ответов


это мой второй ответ, основанный на наследовании одной таблицы.

вопрос содержит пример, где Item подклассы имеют свои собственные атрибуты экземпляра. Например, Pistol является единственным классом в иерархии наследования, который имеет . При представлении этого в базе данных можно сэкономить место, создав таблицу для родительского класса, содержащую столбец для каждого из общих атрибутов, и хранение атрибутов, специфичных для подкласса, в отдельной таблице для каждого подкласса. SQLAlchemy поддерживает это из коробки и называет его наследование присоединенной таблицы (потому что вам нужно объединить таблицы, чтобы собрать как общие атрибуты, так и атрибуты, которые являются конкретными для подкласса). The ответ Ильи Эверилы и мой предыдущий ответ оба предположили, что наследование объединенной таблицы было способом идти.

как выясняется, фактический код Маркуса Месканена немного отличается. Подклассы не имеют определенных атрибутов экземпляра, все они просто имеют - это определитель контекста.

теперь Маркус также прокомментировал, что он не хочет настраивать подклассы для создания сопоставления базы данных с наследованием одной таблицы. Маркус хочет начать с существующей иерархии классов без сопоставления базы данных, а затем создать все отображение базы данных наследования одной таблицы сразу, просто отредактировав базовый класс. Это означало бы, что добавление __mapper_args__ до Sword и Pistol подклассы, как в решении 1B выше, не может быть и речи. Действительно, если disambiguator можно вычислить "автоматически", это экономит много шаблонов, особенно если есть много подклассов.

это можно сделать, используя @declared_attr. Вводят раствор 4:

class Item(Base):
    name = 'unnamed item'

    @classproperty
    def class_id(cls):
        return '.'.join((cls.__module__, cls.__qualname__))

    __tablename__ = 'item'
    id = Column(Integer, primary_key=True)
    type = Column(String(50))
    durability = Column(Integer, default=100)
    ammo = Column(Integer, default=10)

    @declared_attr
    def __mapper_args__(cls):
        if cls == Item:
            return {
                'polymorphic_identity': cls.__name__,
                'polymorphic_on': type,
            }
        else:
            return {
                'polymorphic_identity': cls.__name__,
            }


class Sword(Item):
    name = 'Sword'


class Pistol(Item):
    name = 'Pistol'

это дает тот же результат, что и решение 1B, за исключением того, что значение disambiguator (все еще type столбец) вычисляется из класса вместо произвольно выбранной строки. Здесь это просто название класс (cls.__name__). Вместо этого мы могли бы выбрать полное имя (cls.class_id) или даже пользовательской (cls.name), если вы можете гарантировать, что каждый подкласс переопределяет name. Это действительно не имеет значения, что вы берете в качестве значения определитель контекста, покуда существует взаимнооднозначное соответствие между стоимостью и классом.


цитируя чиновника документация:

когда наш класс построен, декларативный заменяет все Column объекты со специальными средствами доступа Python, известными как дескрипторы; ...

вне того, что процесс отображения делает с нашим классом, класс остается в основном обычным классом Python, к которому мы можем определить любое количество обычных атрибутов и методов, необходимых нашему приложению.

от этого должно быть ясно, что добавление атрибутов класса, методов и т. д. можно. Однако есть определенные зарезервированные имена, а именно __tablename__, __table__, metadata и __mapper_args__ (не исчерпывающий список).

для наследства, SQLAlchemy предлагает три формы: один стол, б и наследование присоединенной таблицы.

реализация упрощенного примера с использованием объединенной таблицы наследство:

class Item(Base):
    name = 'unnamed item'

    @classproperty
    def class_id(cls):
        return '.'.join((cls.__module__, cls.__qualname__))

    __tablename__ = 'item'
    id = Column(Integer, primary_key=True)
    type = Column(String(50))

    __mapper_args__ = {
        'polymorphic_identity': 'item',
        'polymorphic_on': type
    }


class Sword(Item):
    name = 'Sword'

    __tablename__ = 'sword'
    id = Column(Integer, ForeignKey('item.id'), primary_key=True)
    durability = Column(Integer, default=100)

    __mapper_args__ = {
        'polymorphic_identity': 'sword',
    }


class Pistol(Item):
    name = 'Pistol'

    __tablename__ = 'pistol'
    id = Column(Integer, ForeignKey('item.id'), primary_key=True)
    ammo = Column(Integer, default=10)

    __mapper_args__ = {
        'polymorphic_identity': 'pistol',
    }

добавление элементов и запрос:

In [11]: session.add(Pistol())

In [12]: session.add(Pistol())

In [13]: session.add(Sword())

In [14]: session.add(Sword())

In [15]: session.add(Sword(durability=50))

In [16]: session.commit()

In [17]: session.query(Item).all()
Out[17]: 
[<__main__.Pistol at 0x7fce3fd706d8>,
 <__main__.Pistol at 0x7fce3fd70748>,
 <__main__.Sword at 0x7fce3fd709b0>,
 <__main__.Sword at 0x7fce3fd70a20>,
 <__main__.Sword at 0x7fce3fd70a90>]

In [18]: _[-1].durability
Out[18]: 50

In [19]: item =session.query(Item).first()

In [20]: item.name
Out[20]: 'Pistol'

In [21]: item.class_id
Out[21]: '__main__.Pistol'

на ответ Ильи Эверила уже является наилучшим. Пока он не хранит значение class_id в таблице буквально, пожалуйста, обратите внимание, что любые два экземпляра одного класса всегда имеют одинаковое значение class_id. Таким образом, знание класса достаточно для вычислить the class_id для любого данного пункта. В примере кода, который предоставил Ilja,type столбец гарантирует, что класс всегда может быть известен и class_id класс об остальном заботится собственность. Так что class_id по-прежнему в лице в таблице, если косвенно.

я повторяю пример Ильи из его первоначального ответа здесь, в случае, если он решит изменить его в своем собственном посте. Назовем это "решением 1".

class Item(Base):
    name = 'unnamed item'

    @classproperty
    def class_id(cls):
        return '.'.join((cls.__module__, cls.__qualname__))

    __tablename__ = 'item'
    id = Column(Integer, primary_key=True)
    type = Column(String(50))

    __mapper_args__ = {
        'polymorphic_identity': 'item',
        'polymorphic_on': type
    }


class Sword(Item):
    name = 'Sword'

    __tablename__ = 'sword'
    id = Column(Integer, ForeignKey('item.id'), primary_key=True)
    durability = Column(Integer, default=100)

    __mapper_args__ = {
        'polymorphic_identity': 'sword',
    }


class Pistol(Item):
    name = 'Pistol'

    __tablename__ = 'pistol'
    id = Column(Integer, ForeignKey('item.id'), primary_key=True)
    ammo = Column(Integer, default=10)

    __mapper_args__ = {
        'polymorphic_identity': 'pistol',
    }

Илья намекнул на решение в своем последнем комментарии к вопросу, используя @declared_attr, который б буквально хранить class_id внутри стола, но я думаю, что это было бы менее элегантно. Все он покупает вас, представляя ту же самую информацию немного по-другому, ценой усложнения вашего кода. Смотрите сами ("решение 2"):

class Item(Base):
    name = 'unnamed item'

    @classproperty
    def class_id_(cls):  # note the trailing underscore!
        return '.'.join((cls.__module__, cls.__qualname__))

    __tablename__ = 'item'
    id = Column(Integer, primary_key=True)
    class_id = Column(String(50))  # note: NO trailing underscore!

    @declared_attr  # the trick
    def __mapper_args__(cls):
        return {
            'polymorphic_identity': cls.class_id_,
            'polymorphic_on': class_id
        }


class Sword(Item):
    name = 'Sword'

    __tablename__ = 'sword'
    id = Column(Integer, ForeignKey('item.id'), primary_key=True)
    durability = Column(Integer, default=100)

    @declared_attr
    def __mapper_args__(cls):
        return {
            'polymorphic_identity': cls.class_id_,
        }


class Pistol(Item):
    name = 'Pistol'

    __tablename__ = 'pistol'
    id = Column(Integer, ForeignKey('item.id'), primary_key=True)
    ammo = Column(Integer, default=10)

    @declared_attr
    def __mapper_args__(cls):
        return {
            'polymorphic_identity': cls.class_id_,
        }

в этом подходе также есть дополнительная опасность, о которой я расскажу позже.

на мой взгляд, было бы более элегантно, чтобы сделать код проще. Этого можно достичь, начав с решения 1, а затем объединив name и type свойства, так как они являются избыточными ("решение 3"):

class Item(Base):
    @classproperty
    def class_id(cls):
        return '.'.join((cls.__module__, cls.__qualname__))

    __tablename__ = 'item'
    id = Column(Integer, primary_key=True)
    name = Column(String(50))  # formerly known as type

    __mapper_args__ = {
        'polymorphic_identity': 'unnamed item',
        'polymorphic_on': name,
    }


class Sword(Item):
    __tablename__ = 'sword'
    id = Column(Integer, ForeignKey('item.id'), primary_key=True)
    durability = Column(Integer, default=100)

    __mapper_args__ = {
        'polymorphic_identity': 'Sword',
    }


class Pistol(Item):
    __tablename__ = 'pistol'
    id = Column(Integer, ForeignKey('item.id'), primary_key=True)
    ammo = Column(Integer, default=10)

    __mapper_args__ = {
        'polymorphic_identity': 'Pistol',
    }

все три решения, рассмотренные до сих пор, дают вам точно такое же запрошенное поведение на стороне Python (предполагая, что вы проигнорируете ). Например, экземпляр Pistol вернутся 'yourmodule.Pistol' как его class_id и 'Pistol' как его name в каждом решении. Также в каждом решении, если вы добавляете новый класс элемента в иерархию, скажите Key, все его экземпляры будут автоматически сообщать об их class_id на 'yourmodule.Key' и вы сможете установить их общие name раз на уровне класса.

на стороне SQL есть некоторые тонкие различия в отношении имени и значения столбца, который не различается между классами элементов. В решении 1 столбец называется type и его значение выбирается произвольно для каждого класса. В решении 2 имя столбца -class_id и его значение равно свойству class, которое зависит от имени класса. В решении 3 имя name и его значение равно name свойство класса, которое может изменяться независимо от названия класса. Однако, поскольку все эти различные способы устранения неоднозначности класса элементов могут быть сопоставлены один к одному, они содержат одну и ту же информацию.

я упоминал ранее, что есть уловка в том, как решение 2 устраняет неоднозначность класса элемента. Предположим, вы решили переименовать Pistol класс Gun. Gun.class_id_ (С завершающим подчеркиванием) и Gun.__mapper_args__['polymorphic_identity'] будет автоматически изменить на 'yourmodule.Gun'. Однако class_id столбец в вашей базе данных (сопоставлен с Gun.class_id без символа подчеркивания) по-прежнему будет содержать 'yourmodule.Pistol'. Средство миграции базы данных может быть недостаточно умным, чтобы понять, что эти значения необходимо обновить. Если вы не будете осторожны, ваш class_idS будет поврежден, и SQLAlchemy, вероятно, бросит исключения на вас за неспособность найти соответствующие классы для ваших элементов.

вы можете избежать этой проблемы, используя произвольное значение, как определитель контекста, а в растворе 1, и хранения class_id в отдельной колонке с помощью @declared_attr magic (или аналогичный косвенный маршрут), как в решении 2. Однако на данный момент вам действительно нужно спросить себя, почему class_id должен быть в таблице базы данных. Это действительно оправдывает усложнение кода?

возьмите домой сообщение вы can отображение атрибутов простого класса, а также вычисляемых свойств класса с помощью SQLAlchemy, даже перед лицом наследования, как показывают решения. это не обязательно означает, что вы действительно должны это сделать. Начните с конечных целей и найдите самый простой способ их достижения. Только сделайте свое решение более сложным, если это решит реальную проблему.