假设我正在制作一个游戏,里面有一些物品(想想我的经验之谈,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))但是,由于我不想每次都给我的剑和手枪命名(因为名称总是相同的),而且我希望创建新的项目类非常容易,所以我想让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 )?比方说,我想存储一个项目的持久性(实例属性)和名称(类属性),并将其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
)发布于 2016-07-29 23:21:34
这是基于单表继承的第二个答案。
这个问题包含一个示例,其中Item子类有它们自己的特定实例属性。例如,Pistol是继承层次结构中唯一具有ammo属性的类。在数据库中表示此值时,可以为父类创建包含每个公共属性的列的表,并将特定于子类的属性存储在每个子类的单独表中,从而节省空间。SQLAlchemy支持这一点,并将其称为联合表继承 (因为您需要连接表来收集公共属性和子类特有的属性)。Ilja Everil的答复和我先前的回答都认为联合表继承是可行的。
事实证明,Markus Meskanen的实际代码有点不同。子类没有特定的实例属性,它们都有一个共同的level属性。还有,Markus评论说,他希望所有的子类都存储在同一个表中。。使用单个表的一个可能的优点是,您可以添加和删除子类,而无需每次对数据库模式进行重大更改。
SQLAlchemy也提供了对此的支持,它被称为单表继承。如果子类确实有特定的属性,它甚至可以工作。它的效率有点低,因为每行都必须存储每个可能的属性,即使它属于不同子类的一个项。
下面是与我以前的答案(最初是从Ilja的回答复制的)的解决方案1的一个稍微不同的版本。此版本(“解决方案1B")使用单个表继承,因此所有项都存储在同一个表中。
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列是消歧器时,这就是我的意思。
现在,使用Markus还评论说他不想自定义子类。来创建具有单表继承的数据库映射。Markus希望从没有数据库映射的现有类层次结构开始,然后通过编辑基类立即创建整个单表继承数据库映射。这意味着将__mapper_args__添加到Sword和Pistol子类(如上面的解决方案1B中)是不可能的。实际上,如果可以“自动”计算消歧器,这就节省了大量样板,特别是在有许多子类的情况下。
这可以使用@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。只要在值和类之间存在一对一的映射,您认为什么才是消歧器的值并不重要。
发布于 2016-07-22 12:31:34
引用官方文档的话
在构造类时,声明式用称为描述符的特殊
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'发布于 2016-07-29 00:09:41
Ilja Everil的答复已经是最好的可能。虽然它没有将class_id的值按字面顺序存储在表中,但请注意,同一个类的任何两个实例都具有相同的class_id值。因此,知道这个类就足以计算任何给定项的class_id。在Ilja提供的代码示例中,type列确保类总是已知的,class_id类属性负责其余部分。因此,如果是间接的,class_id仍然在表中。
我重复Ilja的例子,从他原来的答案这里,以防他决定改变在他自己的帖子。让我们称之为“解决方案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',
}Ilja在他对这个问题的最后评论中暗示了一个解决方案,使用@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端给出了完全相同的请求行为(假设您将忽略type属性)。例如,Pistol的一个实例将在每个解决方案中返回'yourmodule.Pistol'作为其class_id,'Pistol'作为其name。在每个解决方案中,如果您向层次结构(比如Key )添加了一个新的item类,那么它的所有实例都将自动将其class_id报告为'yourmodule.Key',并且您将能够在类级别设置它们的公共name一次。
在SQL端,对于消除项类之间歧义的列的名称和值,有一些细微的差异。在解决方案1中,该列被称为type,它的值是任意为每个类选择的。在解决方案2中,列名是class_id,其值等于类属性,这取决于类名。在解决方案3中,名称是name,其值等于类的name属性,可以独立于类名进行更改。但是,由于所有这些消除item类歧义的不同方法都可以一对一地映射到彼此,所以它们包含相同的信息。
我在前面提到过,解决方案2消除item类歧义的方式存在一个问题。假设您决定将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魔术(或类似的间接路由)将@declared_attr存储在单独的列中(或类似的间接路由)来避免这一问题,就像在解决方案2中一样。然而,在这一点上,您确实需要问自己为什么class_id需要在数据库表中。这真的有理由让你的代码变得如此复杂吗?
带回家消息:您可以使用SQLAlchemy映射普通的类属性以及计算的类属性,甚至在继承的情况下也是如此,如解决方案所示。但这并不一定意味着你真的应该这么做。从你的最终目标开始,找出最简单的方法来实现这些目标。只有这样做才能解决真正的问题,才能使你的解决方案更加复杂。
https://stackoverflow.com/questions/38519349
复制相似问题