如何将SQLAlchemy与类属性(和属性)一起使用?

时间:2016-07-22 06:12:42

标签: python python-3.x properties sqlalchemy class-attributes

说我正在制作带有物品的游戏(想想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
)

3 个答案:

答案 0 :(得分:2)

引用官方documentation

  

构造我们的类时,Declarative将所有Column对象替换为称为描述符的特殊Python访问器; ...

     

除了映射过程对我们的类所做的之外,该类仍然主要是一个普通的Python类,我们可以定义我们的应用程序所需的任意数量的普通属性和方法。

从中应该清楚的是,添加类属性,方法等是可能的。但是有一些保留名称,即__tablename____table__metadata__mapper_args__(不是详尽的列表)。

至于继承,SQLAlchemy offers three formssingle tableconcretejoined 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进行比较时,有一些事情脱颖而出。 durabilityammo属性已移至Item基类,因此Item或其中一个子类的每个实例现在都具有durabilityammoSwordPistol子类已失去__tablename__个及其所有列属性。这告诉SQLAlchemy SwordPistol没有自己的关联表;换句话说,我们想要使用单表继承。 Item.type列属性和__mapper_args__业务仍然存在;这些为SQLAlchemy提供信息,以确定item表中的任何给定行是否属于ItemSwordPistol类。当我说type列是 disambiguator 时,这就是我的意思。

现在,Markus also commented he does not want to customize the subclasses为了创建具有单表继承的数据库映射。 Markus希望从没有数据库映射的现有类层次结构开始,然后通过编辑基类立即创建整个单表继承数据库映射。这意味着将__mapper_args__添加到SwordPistol子类,就像上面的解决方案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开始然后合并nametype属性来实现,因为它们是多余的(&#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类重命名为GunGun.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映射普通类属性和计算类属性,如解决方案所示。 这并不一定意味着你应该真的这样做。从头脑开始,找到实现这些目标的最简单方法。只有这样才能使您的解决方案变得更加复杂,从而解决了一个真正的问题。