说我正在制作带有物品的游戏(想想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))
但是因为我不想每次都给我的剑和手枪命名(因为名字总是一样的),而且我希望它能够非常容易地创建新的项目类,我想我和我#39; d使name
成为一个类属性:
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
但是有没有办法以这种或那种方式使用SQLAlchemy这些类属性(有时甚至是classproperties)?比如,我想将项目的持久性(实例属性)和名称(类属性)与其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_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
)
答案 0 :(得分:2)
引用官方documentation:
构造我们的类时,Declarative将所有
Column
对象替换为称为描述符的特殊Python访问器; ...除了映射过程对我们的类所做的之外,该类仍然主要是一个普通的Python类,我们可以定义我们的应用程序所需的任意数量的普通属性和方法。
从中应该清楚的是,添加类属性,方法等是可能的。但是有一些保留名称,即__tablename__
,__table__
,metadata
和__mapper_args__
(不是详尽的列表)。
至于继承,SQLAlchemy offers three forms:single table,concrete和joined table inheritance。
使用联接表继承实现简化示例:
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'
答案 1 :(得分:2)
这是我的第二个答案,基于单表继承。
该问题包含一个示例,其中Item
子类具有自己的特定实例属性。例如,Pistol
是继承层次结构中唯一具有ammo
属性的类。在数据库中表示时,可以通过为父类创建一个表来节省空间,该父类包含每个公共属性的列,并在每个子类的单独表中存储特定于子类的属性。 SQLAlchemy支持开箱即用,并将其称为joined table inheritance(因为您需要连接表以便收集公共属性和子类特有的属性)。 answer by Ilja Everilä和my previous answer都认为联接表继承是最佳选择。
事实证明,Markus Meskanen's actual code有点不同。子类没有特定的实例属性,它们只有一个共同的level
属性。另外,Markus commented that he wants all subclasses to be stored in the same table。使用单个表的一个可能的优点是,您可以添加和删除子类,而不会每次都对数据库模式进行重大更改。
SQLAlchemy也为此提供了支持,它被称为single table inheritance。如果子类 do 具有特定属性,它甚至可以工作。效率稍差,因为即使属于不同子类的项目,每一行都必须存储每个可能的属性。
以下是我之前的答案(最初从Ilja's answer复制)的解决方案1的略微更改版本。此版本(&#34;解决方案1B&#34;)使用单表继承,因此所有项目都存储在同一个表中。
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)
__mapper_args__ = {
'polymorphic_identity': 'item',
'polymorphic_on': type
}
class Sword(Item):
name = 'Sword'
__mapper_args__ = {
'polymorphic_identity': 'sword',
}
class Pistol(Item):
name = 'Pistol'
__mapper_args__ = {
'polymorphic_identity': 'pistol',
}
当我们将其与原始解决方案1进行比较时,有一些事情脱颖而出。 durability
和ammo
属性已移至Item
基类,因此Item
或其中一个子类的每个实例现在都具有durability
和ammo
。 Sword
和Pistol
子类已失去__tablename__
个及其所有列属性。这告诉SQLAlchemy Sword
和Pistol
没有自己的关联表;换句话说,我们想要使用单表继承。 Item.type
列属性和__mapper_args__
业务仍然存在;这些为SQLAlchemy提供信息,以确定item
表中的任何给定行是否属于Item
,Sword
或Pistol
类。当我说type
列是 disambiguator 时,这就是我的意思。
现在,Markus also commented he does not want to customize the subclasses为了创建具有单表继承的数据库映射。 Markus希望从没有数据库映射的现有类层次结构开始,然后通过编辑基类立即创建整个单表继承数据库映射。这意味着将__mapper_args__
添加到Sword
和Pistol
子类,就像上面的解决方案1B一样,是不可能的。实际上,如果可以自动计算消除歧义并且#34;这节省了大量的样板,特别是如果有许多子类。
可以使用@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相同的结果,除了消除歧义器(仍为type
列)的值是从类计算而不是任意选择的字符串。这里,它只是类的名称(cls.__name__
)。如果您可以保证每个子类都覆盖cls.class_id
,我们可以选择完全限定名称(name
)或甚至自定义cls.name
属性(name
)。只要在值和类之间存在一对一的映射,你所采用的消歧器的价值并不重要。
答案 2 :(得分:1)
answer by Ilja Everilä已经是最好的了。虽然它没有将class_id
的值存储在表字面中,但请注意,同一类的任何两个实例始终具有相同的class_id
值。因此,了解该类就足以计算任何给定项目的class_id
。在Ilja提供的代码示例中,type
列确保始终可以知道类,class_id
类属性可以处理其余的事务。因此,class_id
在表格中仍然是代表,如果是间接的话。
我从他原来的答案重复Ilja的例子,以防他决定在自己的帖子中改变它。我们称之为&#34;解决方案1&#34;。
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',
}
Ilja在他对该问题的最后评论中暗示了一个解决方案,使用@declared_attr
,其中 将class_id
字面存储在表格中,但我认为它会是不太优雅。它所购买的只是以稍微不同的方式表示完全相同的信息,代价是使代码更复杂。亲眼看看(&#34;解决方案2&#34;):
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
属性来实现,因为它们是多余的(&#34;解决方案3&#34;):
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方面完全相同的请求行为(假设您将忽略type
属性)。例如,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
将被破坏,SQLAlchemy可能会因你无法找到你的项目的匹配类而向你抛出异常。
您可以通过使用任意值作为消除歧义来避免此问题,如解决方案1中所示,并使用class_id
magic(或类似的间接路由)将@declared_attr
存储在单独的列中,如在解决方案2.但是,此时您真的需要问问自己为什么class_id
需要在数据库表中。是否真的有理由让你的代码变得如此复杂?
带回家消息:即使面对继承,也可以使用SQLAlchemy映射普通类属性和计算类属性,如解决方案所示。 这并不一定意味着你应该真的这样做。从头脑开始,找到实现这些目标的最简单方法。只有这样才能使您的解决方案变得更加复杂,从而解决了一个真正的问题。