10

假设我正在制作一个包含物品的游戏(想想 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))

但是因为我不想每次都给我的剑和手枪命名(因为名字总是一样的),而且我希望创建新的物品类非常容易,我想我会做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

但是有没有办法以一种或另一种方式将这些类属性(有时甚至是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_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
)
4

3 回答 3

5

这是我的第二个答案,基于单表继承。

该问题包含一个示例,其中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,因此其子类或其子类的每个实例现在都具有 a和 an 。和子类失去了它们的 s以及它们的所有列属性。这告诉 SQLAlchemy并且没有自己的关联表;换句话说,我们要使用单表继承。列属性和业务还在;这些为 SQLAlchemy 提供信息以确定表中的任何给定行是否属于,或ammoItemItemdurabilityammoSwordPistol__tablename__SwordPistolItem.type__mapper_args__itemItemSwordPistol班级。当我说该type列是消歧器时,这就是我的意思。

现在,Markus 还评论说他不想自定义子类以创建具有单表继承的数据库映射。Markus 想要从没有数据库映射的现有类层次结构开始,然后通过编辑基类立即创建整个单表继承数据库映射。这意味着像上面的解决方案 1B 一样,添加__mapper_args__Sword和子类是不可能的。Pistol实际上,如果可以“自动”计算消歧器,这可以节省大量样板文件,尤其是在有许多子类的情况下。

这可以使用@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覆盖. 只要值和类之间存在一对一的映射,您将什么作为消歧器的值并不重要。namecls.namename

于 2016-07-29T23:21:34.170 回答
2

引用官方文档

当我们的类被构造时,Declarative 用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-22T12:31:34.037 回答
1

Ilja Everilä的答案已经是最好的了。虽然它没有按字面意思class_id将 的值存储在表中,但请注意同一类的任何两个实例始终具有相同的 值。因此,知道类足以计算任何给定项目的。在 Ilja 提供的代码示例中,该列确保始终可以知道类,而类属性负责其余部分。因此,如果是间接的,则仍然在表中表示。class_idclass_idtypeclass_idclass_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 在他对这个问题的最后评论中暗示了一个解决方案, using @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 开始然后合并nameandtype属性来实现,因为它们是多余的(“解决方案 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,它的所有实例将自动报告它们class_id的存在'yourmodule.Key',并且您将能够name在类级别设置它们的共同点。

SQL 方面有一些细微的差异,关于在项目类之间消除歧义的列的名称和值。在解决方案 1 中,调用该列type并为每个类任意选择其值。方案二中,列名是class_id,其值等于类属性,这取决于类名。在解决方案 3 中,名称 isname并且其值等于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,如在解决方案 2 中。但是,此时您确实需要问问自己为什么class_id需要在数据库表中。它真的有理由让你的代码如此复杂吗?

带回家的信息:您可以使用 SQLAlchemy 映射普通类属性以及计算类属性,即使面对继承,如解决方案所示。这并不一定意味着你真的应该这样做。从您的最终目标开始,并找到实现这些目标的最简单方法。只有在解决实际问题的情况下才能使您的解决方案更加复杂。

于 2016-07-29T00:09:41.730 回答