SQLAlchemy 踩坑:我只想复制一张表,却意外触发了“联表继承”
在 Python 后端开发中,我们经常使用 SQLAlchemy 作为 ORM 框架。最近在做项目时,我遇到了一件非常有意思的事情,或者说是一个“经典的误解”。
事情是这样的:我有一个现成的文件表 File,它的结构定义得很完美。随着业务发展,我需要创建一个备份表 BackupFile,要求它的结构跟 File 表 完全一致。
作为一个崇尚 DRY (Don’t Repeat Yourself) 原则的程序员,我的第一反应当然是——继承!
于是我写出了类似这样的代码:
1 | from sqlalchemy import Column, Integer, String, ForeignKey |
当我兴致勃勃地去生成数据库表时,却发现 backup_file 表的结构非常奇怪:它里面并没有 filename 和 path 字段,只有一个 id 字段,而且这个 id 还是 file 表的外键!
这时候大佬告诉我:“兄弟,你这触发了 SQLAlchemy 的 **联表继承 (Joined Table Inheritance)**。”
这就触及到我的知识盲区了。我只想复制个表,怎么就“联表继承”了?这篇博客就来把这个概念由浅入深地讲清楚。
什么是联表继承 (Joined Table Inheritance)?
在面向对象编程 (OOP) 中,继承意味着“子类是父类的一种特殊形态”。比如 Dog 继承自 Animal。
SQLAlchemy 作为一个优秀的 ORM,它试图在关系型数据库中还原这种面向对象的关系。当你让一个 Model 类直接继承另一个 Model 类时,SQLAlchemy 会默认你是在做 数据层面的继承,而不是简单的 代码复用。
它在数据库里长什么样?
在上面的例子中,SQLAlchemy 是这样理解的:
-
File是父类,存储所有文件的通用信息(如filename,path)。 -
BackupFile是子类,它是File的一种特殊形式。
因此,它生成的表结构逻辑如下:
- file 表:存储
id,filename,path。 - backup_file 表:只存储它自己特有的字段(在我的代码里没有定义特有字段)以及一个指向父表的主键 (
id)。
数据的存储与查询
这种结构下,数据是分散存储的。
当你创建一个 BackupFile 对象并保存时:
- 公共字段(
filename等)的数据会被存入file表。 - 特有字段的数据会被存入
backup_file表。 - 两张表通过主键
id关联。
当你查询 BackupFile 时:
1 | # 查询 BackupFile |
SQLAlchemy 会自动生成一个 JOIN 语句,把 file 表和 backup_file 表连接起来,把你需要的完整数据拼凑给你。
为什么这不符合“复制表”的需求?
虽然“联表继承”在处理真正的继承关系(如 User -> AdminUser, RegularUser)时非常有用,但对于“我想创建一个完全独立的备份表”这个需求来说,它是个大坑:
- 数据耦合:
BackupFile的数据实际上依赖于file表。如果你删除了file表里的一行,对应的BackupFile数据也会失去意义(或者被级联删除)。 - 性能损耗:每次查询
BackupFile都要进行JOIN操作,而我们原本只需要一张独立的单表。 - 逻辑错误:备份表通常应该是独立的快照,不应该跟原表共享同一行物理数据。
正确的做法:抽象基类 (Abstract Base Class)
既然直接继承 Model 类会导致“联表继承”,那我们该怎么做才能既复用代码,又生成两张完全独立的表呢?
答案是使用 抽象基类 或 Mixin。
我们需要告诉 SQLAlchemy:“这个父类只是一个模板(Template),不要把它当成数据库里的一张实体表。”
在 SQLAlchemy 中,我们可以通过设置 __abstract__ = True 来实现。
代码修正
1 | # 定义一个抽象基类,它不会在数据库生成表 |
结果对比
现在,SQLAlchemy 会这样处理:
- 看到
BaseFileModel是抽象的,跳过建表。 - 看到
File继承自BaseFileModel,把模板里的字段都拷贝过来,创建一张独立的file表。 - 看到
BackupFile继承自BaseFileModel,同样拷贝字段,创建一张独立的backup_file表。
此时,file 表和 backup_file 表在数据库物理层面上是完全隔离的,互不干扰,但我们在 Python 代码层面又实现了完美的复用。
总结
- Model 继承 Model (
class B(A)):默认触发 联表继承。数据库会有两张表,子表通过外键关联父表。适用于“子类是父类的一种”且需要多态查询的场景。 - Model 继承 Abstract Base (
__abstract__ = True):这是 代码复用。数据库会生成两张完全独立的表,结构一致。这才是“复制表结构”的正确姿势。
下次想“复制”表的时候,千万别手滑直接继承了,记得多加一个 __abstract__ = True 的中间层!




