ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用一局太空侵略者看懂 SQLAlchemy ORM:多态继承与 joinedload 预加载实战

用一局太空侵略者看懂 SQLAlchemy ORM:多态继承与 joinedload 预加载实战

用一局太空侵略者看懂 SQLAlchemy ORM:多态继承与 joinedload 预加载实战

【免费下载链接】sqlalchemyThe Database Toolkit for Python项目地址: https://gitcode.com/gh_mirrors/sq/sqlalchemy

SQLAlchemy ORM 常被当成 CRUD 脚手架,但仓库里藏着一个比较深的例子:一个用终端 curses 画出来的太空侵略者游戏,运行状态、碰撞判断、动画刷新全部压在 ORM 之上。我们以它为例倒推三件事——如何用一张表装下多种敌人、如何把碰撞检测写成一条 SQL,以及为什么一个joinedload能救回一批查询。

先跑起来:500 行终端射击游戏背后

例子在 space_invaders.py 里,单文件、无第三方依赖,跑起来就是一局可玩的射击游戏。建库和建表只用了四行:

e = create_engine("sqlite://") Base.metadata.create_all(e) session = Session(e) init_glyph(session) session.commit()

sqlite://是纯内存库,create_all一次建完所有表,之后对象都挂在同一个Session上。游戏每 10ms 刷新一次,而每帧"场上有什么"都是从数据库查出来的——这正是它作为 ORM 案例的意义所在。

单表多态:一张 glyph 表装下所有敌人

游戏里有军队、UFO、玩家船、导弹、爆炸、提示文字,类型很多,ORM 却用单表继承把它们塞进同一张glyph表。

polymorphic_on 就是类型判别列

class Glyph(Base): __tablename__ = "glyph" id = Column(Integer, primary_key=True) type = Column(String) __mapper_args__ = {"polymorphic_on": type} class ArmyGlyph(EnemyGlyph): __mapper_args__ = {"polymorphic_identity": "army"} class GlyphCoordinate(Base): __tablename__ = "glyph_coordinate" glyph_id = Column(Integer, ForeignKey("glyph.id")) x = Column(Integer) y = Column(Integer) glyph = relationship(Glyph, innerjoin=True)

polymorphic_on指定type列作判别列,polymorphic_identity给每个子类一个字符串标签。查出来时 ORM 按type的值把行还原成正确的子类,ArmyGlyph、SaucerGlyph各自带上自己的方法。更多规则见 inheritance.rst。

把碰撞检测写进 SQL:of_type 与 hybrid_method

常规做法是把坐标加载进内存,用 Python 逐个判断矩形是否相交。这里反了过来,让数据库来做几何判断。

intersects 把判断下推到数据库

@hybrid_method def intersects(self, other): return ~( (self.x + self.width < other.x) | (self.x > other.x + other.width) ) & ~( (self.y + self.height < other.y) | (self.y > other.y + other.height) )

hybrid_method让它在 Python 端和 SQL 端都能用,于是判负逻辑可以整段下推:

def check_lose(session, state): player = state["player"] return ( session.query(GlyphCoordinate) .join(GlyphCoordinate.glyph.of_type(ArmyGlyph)) .filter( GlyphCoordinate.intersects(player) | GlyphCoordinate.bottom_bound ) .count() )

of_type(ArmyGlyph)把继承查询收窄到"军队"这一支,再配合intersects生成一条带几何条件的COUNT,命中即判负。一次往返,没有把全部坐标拉回内存。

调优:joinedload 如何救你的 N+1

渲染阶段如果不用预加载,每访问一次gcoord.glyph就多发一条查询,十几个敌人就是十几条额外往返。

joinedload 一次 JOIN 取回关联

def draw(session, window, state): for gcoord in session.query(GlyphCoordinate).options( joinedload(GlyphCoordinate.glyph) ): gcoord.render(window, state)

joinedload让glyph_coordinate与glyph一次 JOIN 取回,把 N+1 压回单条 SQL。策略取舍可参考 loading_relationships.rst。对这种小结果集、高频刷新的场景,joinedload基本是默认最优解;行数极大时再换subqueryload/selectinload。

下一步

想继续拆,可以顺着 space_invaders.py 看move_army如何用func.min/func.max算出军队边界并整体平移;或把单表继承换成 joined-table,对比多出来的 JOIN。这个例子的价值在于,它把多态、hybrid、预加载几件事压缩进了一个能跑、能玩、能看 SQL 的闭环里。

【免费下载链接】sqlalchemyThe Database Toolkit for Python项目地址: https://gitcode.com/gh_mirrors/sq/sqlalchemy

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进