Как использовать 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_id
S будет поврежден, и SQLAlchemy, вероятно, бросит исключения на вас за неспособность найти соответствующие классы для ваших элементов.
вы можете избежать этой проблемы, используя произвольное значение, как определитель контекста, а в растворе 1, и хранения class_id
в отдельной колонке с помощью @declared_attr
magic (или аналогичный косвенный маршрут), как в решении 2. Однако на данный момент вам действительно нужно спросить себя, почему class_id
должен быть в таблице базы данных. Это действительно оправдывает усложнение кода?
возьмите домой сообщение вы can отображение атрибутов простого класса, а также вычисляемых свойств класса с помощью SQLAlchemy, даже перед лицом наследования, как показывают решения. это не обязательно означает, что вы действительно должны это сделать. Начните с конечных целей и найдите самый простой способ их достижения. Только сделайте свое решение более сложным, если это решит реальную проблему.