如何使用具有 class 属性(和特性)的 SQLAlchemy?
How to use SQLAlchemy with class attributes (and properties)?
假设我正在制作一款包含物品的游戏(想想 Minecraft、CS:GO 武器、英雄联盟和 Dota 物品等)。游戏中可能有大量相同的物品,但细节差异很小,例如 condition/durability 或物品中剩余的弹药量:
player1.give_item(Sword(name='Sword', durability=50))
player2.give_item(Sword(name='Sword', durability=80))
player2.give_item(Pistol(name='Pistol', ammo=12))
但是因为我不想每次都给我的剑和手枪命名(因为名字总是一样的),而且我希望创建新物品非常容易class是的,我想我会让 name
成为 class 属性:
class Item:
name = 'unnamed item'
现在我简单地子class这个:
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
而且我们正在工作 classes:
>>> sword = Sword(30)
>>> print(sword.name, sword.durability, sep=', ')
Sword, 30
但是有没有办法以某种方式将这些 class 属性(有时甚至是 classproperties)与 SQLAlchemy 一起使用?比如说,我想以 class_id
(class 属性) 作为主键来存储一个项目的耐久性(实例属性)和名称(class 属性):
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 属性 怎么样?
实际上我有更大的继承树,每个 class 有多个 attributes/properties 以及更多的实例属性。
更新: 我在 post 中不清楚 table。我只想为项目设置 one table,其中 class_id
用作主键。这就是我用元数据构建 table 的方式:
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
)
引用官方documentation:
When our class is constructed, Declarative replaces all the Column
objects with special Python accessors known as descriptors; ...
Outside of what the mapping process does to our class, the class remains otherwise mostly a normal Python class, to which we can define any number of ordinary attributes and methods needed by our application.
从这里应该清楚,添加 class 属性、方法等是可能的。虽然有某些保留名称,即 __tablename__
、__table__
、metadata
和 __mapper_args__
(并非详尽列表)。
至于继承,SQLAlchemy offers three forms: single table, concrete and joined table inheritance。
使用连接的 table 继承实现您的简化示例:
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
的值存储在 table 字面上 中,但请注意相同 class 的任何两个实例始终具有相同的值class_id
的值。因此,知道 class 足以 计算 任何给定项目的 class_id
。在 Ilja 提供的代码示例中,type
列确保 class 始终是已知的,而 class_id
class 属性 负责其余部分。所以 class_id
仍然 在 table 中表示 ,如果是间接的。
我在这里重复 Ilja 的原始答案中的示例,以防他决定自己更改它 post。让我们称之为 "solution 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
存储在 table,但我认为它会不那么优雅。它为您带来的只是以稍微不同的方式表示完全相同的信息,代价是使您的代码更加复杂。自己看看 ("solution 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
属性来实现,因为它们是多余的 ("solution 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
的实例将 return 'yourmodule.Pistol'
作为其 class_id
和 'Pistol'
作为其每个解决方案中的 name
。同样在每个解决方案中,如果您向层次结构添加一个新项目 class,比如 Key
,它的所有实例将自动报告它们的 class_id
为 'yourmodule.Key'
,您将能够在 class 级别设置他们的共同 name
一次。
SQL 方面存在一些细微差异,关于在项目 class 之间消除歧义的列的名称和值。在解决方案 1 中,该列称为 type
,其值是为每个 class 任意选择的。解决方案2中,列名是class_id
,其值等于class 属性,这取决于class名称。在解决方案3中,名称为name
,其值等于class的name
属性,可以独立于class名称而变化.但是,由于所有这些消除项目歧义的不同方法 class 可以相互一对一映射,因此它们包含相同的信息。
我之前提到,解决方案 2 消除项目 class 歧义的方式存在问题。假设您决定将 Pistol
class 重命名为 Gun
。 Gun.class_id_
(尾随下划线)和 Gun.__mapper_args__['polymorphic_identity']
将自动更改为 'yourmodule.Gun'
。但是,数据库中的 class_id
列(映射到 Gun.class_id
且末尾没有下划线)仍将包含 'yourmodule.Pistol'
。您的数据库迁移工具可能不够智能,无法确定需要更新这些值。如果您不小心,您的 class_id
将被损坏,并且 SQLAlchemy 可能会因为无法为您的项目找到匹配的 class 向您抛出异常。
您可以通过使用任意值作为消歧器来避免此问题,如解决方案 1 中所示,并使用 @declared_attr
魔术(或类似的间接路由)将 class_id
存储在单独的列中,如解决方案 2。但是,此时您真的需要问自己为什么 class_id
需要在数据库 table 中。让您的代码如此复杂真的合理吗?
带回家的消息:您可以映射普通class属性以及使用[计算的class属性SQL炼金术,即使面对继承,如解决方案所示。 这并不一定意味着您真的应该这样做。从您的最终目标开始,找到实现这些目标的最简单方法。只有在解决实际问题的情况下,才使您的解决方案更加复杂。
这是我的第二个答案,基于单一table继承。
该问题包含一个示例,其中 Item
个子class 具有它们自己的特定实例属性。例如,Pistol
是继承层次结构中唯一具有 ammo
属性的 class。在数据库中表示时,您可以通过为父 class 创建一个 table 来保存 space,父 class 包含每个公共属性的列,并存储特定于某个属性的属性subclass 在单独的 table 中用于每个 subclasses。 SQLAlchemy 开箱即用地支持它并将其称为 joined table inheritance (because you need to join tables in order to collect both the common attributes and the attributes that are particular to a subclass). The and 两者都假设加入 table 继承是可行的方法。
事实证明,Markus Meskanen's actual code is a bit different. The subclasses do not have particular instance attributes, they all just have a level
attribute in common. Also, 。使用单个 table 的一个可能的优点是您可以添加和删除子 class,而不会每次都对数据库模式造成重大更改。
SQLAlchemy 也对此提供支持,称为 single table inheritance。如果 subclasses do 具有特定属性,它甚至可以工作。它只是效率低了一点,因为每一行都必须存储每个可能的属性,即使它属于不同子class.
的项目也是如此。
这是我之前回答(最初从 复制而来)的解决方案 1 的略微修改版本。此版本 ("solution 1B") 使用单一 table 继承,因此所有项都存储在相同的 table.
中
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
基础 class,因此 Item
或其子 class 之一的每个实例现在有一个 durability
和一个 ammo
。 Sword
和 Pistol
子 class 已经丢失了它们的 __tablename__
以及它们的所有列属性。这告诉 SQLAlchemy Sword
和 Pistol
没有自己的关联 table;换句话说,我们想要使用单 table 继承。 Item.type
列属性和__mapper_args__
业务还在;这些为 SQLAlchemy 提供信息以确定 item
table 中的任何给定行是否属于 Item
、Sword
或 Pistol
class。这就是我说 type
列是 消歧义符 .
的意思
现在, 为了创建具有单一 table 继承的数据库映射。 Markus 希望从一个没有数据库映射的现有 class 层次结构开始,然后通过编辑基础 class 立即创建整个单个 table 继承数据库映射。这意味着将 __mapper_args__
添加到 Sword
和 Pistol
subclasses,就像上面的解决方案 1B 一样,是不可能的。事实上,如果可以计算消歧器 "automagically",这将节省大量样板代码,尤其是在有很多子 class 的情况下。
这可以使用 @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
列)是根据 class 计算的,而不是任意选择的字符串。在这里,它只是 class (cls.__name__
) 的名称。如果您可以保证每个子 class 覆盖 name
.只要值与 class.
之间存在一对一的映射,您将什么作为消歧器的值并不重要。
假设我正在制作一款包含物品的游戏(想想 Minecraft、CS:GO 武器、英雄联盟和 Dota 物品等)。游戏中可能有大量相同的物品,但细节差异很小,例如 condition/durability 或物品中剩余的弹药量:
player1.give_item(Sword(name='Sword', durability=50))
player2.give_item(Sword(name='Sword', durability=80))
player2.give_item(Pistol(name='Pistol', ammo=12))
但是因为我不想每次都给我的剑和手枪命名(因为名字总是一样的),而且我希望创建新物品非常容易class是的,我想我会让 name
成为 class 属性:
class Item:
name = 'unnamed item'
现在我简单地子class这个:
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
而且我们正在工作 classes:
>>> sword = Sword(30)
>>> print(sword.name, sword.durability, sep=', ')
Sword, 30
但是有没有办法以某种方式将这些 class 属性(有时甚至是 classproperties)与 SQLAlchemy 一起使用?比如说,我想以 class_id
(class 属性) 作为主键来存储一个项目的耐久性(实例属性)和名称(class 属性):
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 属性 怎么样?
实际上我有更大的继承树,每个 class 有多个 attributes/properties 以及更多的实例属性。
更新: 我在 post 中不清楚 table。我只想为项目设置 one table,其中 class_id
用作主键。这就是我用元数据构建 table 的方式:
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
)
引用官方documentation:
When our class is constructed, Declarative replaces all the
Column
objects with special Python accessors known as descriptors; ...Outside of what the mapping process does to our class, the class remains otherwise mostly a normal Python class, to which we can define any number of ordinary attributes and methods needed by our application.
从这里应该清楚,添加 class 属性、方法等是可能的。虽然有某些保留名称,即 __tablename__
、__table__
、metadata
和 __mapper_args__
(并非详尽列表)。
至于继承,SQLAlchemy offers three forms: single table, concrete and joined table inheritance。
使用连接的 table 继承实现您的简化示例:
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
的值存储在 table 字面上 中,但请注意相同 class 的任何两个实例始终具有相同的值class_id
的值。因此,知道 class 足以 计算 任何给定项目的 class_id
。在 Ilja 提供的代码示例中,type
列确保 class 始终是已知的,而 class_id
class 属性 负责其余部分。所以 class_id
仍然 在 table 中表示 ,如果是间接的。
我在这里重复 Ilja 的原始答案中的示例,以防他决定自己更改它 post。让我们称之为 "solution 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
存储在 table,但我认为它会不那么优雅。它为您带来的只是以稍微不同的方式表示完全相同的信息,代价是使您的代码更加复杂。自己看看 ("solution 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
属性来实现,因为它们是多余的 ("solution 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
的实例将 return 'yourmodule.Pistol'
作为其 class_id
和 'Pistol'
作为其每个解决方案中的 name
。同样在每个解决方案中,如果您向层次结构添加一个新项目 class,比如 Key
,它的所有实例将自动报告它们的 class_id
为 'yourmodule.Key'
,您将能够在 class 级别设置他们的共同 name
一次。
SQL 方面存在一些细微差异,关于在项目 class 之间消除歧义的列的名称和值。在解决方案 1 中,该列称为 type
,其值是为每个 class 任意选择的。解决方案2中,列名是class_id
,其值等于class 属性,这取决于class名称。在解决方案3中,名称为name
,其值等于class的name
属性,可以独立于class名称而变化.但是,由于所有这些消除项目歧义的不同方法 class 可以相互一对一映射,因此它们包含相同的信息。
我之前提到,解决方案 2 消除项目 class 歧义的方式存在问题。假设您决定将 Pistol
class 重命名为 Gun
。 Gun.class_id_
(尾随下划线)和 Gun.__mapper_args__['polymorphic_identity']
将自动更改为 'yourmodule.Gun'
。但是,数据库中的 class_id
列(映射到 Gun.class_id
且末尾没有下划线)仍将包含 'yourmodule.Pistol'
。您的数据库迁移工具可能不够智能,无法确定需要更新这些值。如果您不小心,您的 class_id
将被损坏,并且 SQLAlchemy 可能会因为无法为您的项目找到匹配的 class 向您抛出异常。
您可以通过使用任意值作为消歧器来避免此问题,如解决方案 1 中所示,并使用 @declared_attr
魔术(或类似的间接路由)将 class_id
存储在单独的列中,如解决方案 2。但是,此时您真的需要问自己为什么 class_id
需要在数据库 table 中。让您的代码如此复杂真的合理吗?
带回家的消息:您可以映射普通class属性以及使用[计算的class属性SQL炼金术,即使面对继承,如解决方案所示。 这并不一定意味着您真的应该这样做。从您的最终目标开始,找到实现这些目标的最简单方法。只有在解决实际问题的情况下,才使您的解决方案更加复杂。
这是我的第二个答案,基于单一table继承。
该问题包含一个示例,其中 Item
个子class 具有它们自己的特定实例属性。例如,Pistol
是继承层次结构中唯一具有 ammo
属性的 class。在数据库中表示时,您可以通过为父 class 创建一个 table 来保存 space,父 class 包含每个公共属性的列,并存储特定于某个属性的属性subclass 在单独的 table 中用于每个 subclasses。 SQLAlchemy 开箱即用地支持它并将其称为 joined table inheritance (because you need to join tables in order to collect both the common attributes and the attributes that are particular to a subclass). The
事实证明,Markus Meskanen's actual code is a bit different. The subclasses do not have particular instance attributes, they all just have a level
attribute in common. Also,
SQLAlchemy 也对此提供支持,称为 single table inheritance。如果 subclasses do 具有特定属性,它甚至可以工作。它只是效率低了一点,因为每一行都必须存储每个可能的属性,即使它属于不同子class.
的项目也是如此。这是我之前回答(最初从
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
基础 class,因此 Item
或其子 class 之一的每个实例现在有一个 durability
和一个 ammo
。 Sword
和 Pistol
子 class 已经丢失了它们的 __tablename__
以及它们的所有列属性。这告诉 SQLAlchemy Sword
和 Pistol
没有自己的关联 table;换句话说,我们想要使用单 table 继承。 Item.type
列属性和__mapper_args__
业务还在;这些为 SQLAlchemy 提供信息以确定 item
table 中的任何给定行是否属于 Item
、Sword
或 Pistol
class。这就是我说 type
列是 消歧义符 .
现在,__mapper_args__
添加到 Sword
和 Pistol
subclasses,就像上面的解决方案 1B 一样,是不可能的。事实上,如果可以计算消歧器 "automagically",这将节省大量样板代码,尤其是在有很多子 class 的情况下。
这可以使用 @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
列)是根据 class 计算的,而不是任意选择的字符串。在这里,它只是 class (cls.__name__
) 的名称。如果您可以保证每个子 class 覆盖 name
.只要值与 class.