在 Python 后端开发中,我们经常使用 SQLAlchemy 作为 ORM 框架。最近在做项目时,我遇到了一件非常有意思的事情,或者说是一个“经典的误解”。

事情是这样的:我有一个现成的文件表 File,它的结构定义得很完美。随着业务发展,我需要创建一个备份表 BackupFile,要求它的结构跟 File完全一致

作为一个崇尚 DRY (Don’t Repeat Yourself) 原则的程序员,我的第一反应当然是——继承

于是我写出了类似这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import declarative_base

Base = declarative_base()

class File(Base):
__tablename__ = 'file'
id = Column(Integer, primary_key=True)
filename = Column(String(100))
path = Column(String(200))

# 我以为这样就能得到一个结构一样的 backup_file 表
class BackupFile(File):
__tablename__ = 'backup_file'
# 我想着继承了 File,字段应该都有了吧?

当我兴致勃勃地去生成数据库表时,却发现 backup_file 表的结构非常奇怪:它里面并没有 filenamepath 字段,只有一个 id 字段,而且这个 id 还是 file 表的外键!

这时候大佬告诉我:“兄弟,你这触发了 SQLAlchemy 的 **联表继承 (Joined Table Inheritance)**。”

这就触及到我的知识盲区了。我只想复制个表,怎么就“联表继承”了?这篇博客就来把这个概念由浅入深地讲清楚。


什么是联表继承 (Joined Table Inheritance)?

在面向对象编程 (OOP) 中,继承意味着“子类是父类的一种特殊形态”。比如 Dog 继承自 Animal

SQLAlchemy 作为一个优秀的 ORM,它试图在关系型数据库中还原这种面向对象的关系。当你让一个 Model 类直接继承另一个 Model 类时,SQLAlchemy 会默认你是在做 数据层面的继承,而不是简单的 代码复用

它在数据库里长什么样?

在上面的例子中,SQLAlchemy 是这样理解的:

  1. File 是父类,存储所有文件的通用信息(如 filename, path)。
  2. BackupFile 是子类,它是 File 的一种特殊形式。

因此,它生成的表结构逻辑如下:

  • file 表:存储 id, filename, path
  • backup_file 表:只存储它自己特有的字段(在我的代码里没有定义特有字段)以及一个指向父表的主键 (id)。

数据的存储与查询

这种结构下,数据是分散存储的。

当你创建一个 BackupFile 对象并保存时:

  • 公共字段(filename 等)的数据会被存入 file 表。
  • 特有字段的数据会被存入 backup_file 表。
  • 两张表通过主键 id 关联。

当你查询 BackupFile 时:

1
2
# 查询 BackupFile
backup = session.query(BackupFile).first()

SQLAlchemy 会自动生成一个 JOIN 语句,把 file 表和 backup_file 表连接起来,把你需要的完整数据拼凑给你。

为什么这不符合“复制表”的需求?

虽然“联表继承”在处理真正的继承关系(如 User -> AdminUser, RegularUser)时非常有用,但对于“我想创建一个完全独立的备份表”这个需求来说,它是个大坑:

  1. 数据耦合BackupFile 的数据实际上依赖于 file 表。如果你删除了 file 表里的一行,对应的 BackupFile 数据也会失去意义(或者被级联删除)。
  2. 性能损耗:每次查询 BackupFile 都要进行 JOIN 操作,而我们原本只需要一张独立的单表。
  3. 逻辑错误:备份表通常应该是独立的快照,不应该跟原表共享同一行物理数据。

正确的做法:抽象基类 (Abstract Base Class)

既然直接继承 Model 类会导致“联表继承”,那我们该怎么做才能既复用代码,又生成两张完全独立的表呢?

答案是使用 抽象基类Mixin

我们需要告诉 SQLAlchemy:“这个父类只是一个模板(Template),不要把它当成数据库里的一张实体表。”

在 SQLAlchemy 中,我们可以通过设置 __abstract__ = True 来实现。

代码修正

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 定义一个抽象基类,它不会在数据库生成表
class BaseFileModel(Base):
__abstract__ = True # 关键点!

id = Column(Integer, primary_key=True)
filename = Column(String(100))
path = Column(String(200))

# File 表继承这个模板
class File(BaseFileModel):
__tablename__ = 'file'

# BackupFile 表也继承这个模板
class BackupFile(BaseFileModel):
__tablename__ = 'backup_file'

结果对比

现在,SQLAlchemy 会这样处理:

  1. 看到 BaseFileModel 是抽象的,跳过建表。
  2. 看到 File 继承自 BaseFileModel,把模板里的字段都拷贝过来,创建一张独立的 file 表。
  3. 看到 BackupFile 继承自 BaseFileModel,同样拷贝字段,创建一张独立的 backup_file 表。

此时,file 表和 backup_file 表在数据库物理层面上是完全隔离的,互不干扰,但我们在 Python 代码层面又实现了完美的复用。


总结

  • Model 继承 Model (class B(A)):默认触发 联表继承。数据库会有两张表,子表通过外键关联父表。适用于“子类是父类的一种”且需要多态查询的场景。
  • Model 继承 Abstract Base (__abstract__ = True):这是 代码复用。数据库会生成两张完全独立的表,结构一致。这才是“复制表结构”的正确姿势。

下次想“复制”表的时候,千万别手滑直接继承了,记得多加一个 __abstract__ = True 的中间层!