3. 企业级数据建模

企业级 Django 项目的数据层很少只有几张互不相关的表。订单、客户、商品、库存、支付、审计日志之间往往存在多对多、自关联、泛化与特化等复杂关系。如果模型设计阶段没有处理好继承与抽象,后期迁移会变得非常痛苦。本节围绕四类核心手段展开:抽象基类、多表继承、代理模型,以及与之配套的迁移策略。

一、抽象基类:共享字段而不建表

抽象基类用于在多个模型之间复用字段和方法,但它本身不会生成数据库表。子类会把基类字段“复制”到自己的表中。典型场景是时间戳与软删除:

class TimeStampedModel(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True

class Order(TimeStampedModel):
    order_no = models.CharField(max_length=32, unique=True)
    total_amount = models.DecimalField(max_digits=12, decimal_places=2)

执行 makemigrations 后,只会为 Order 生成一张表,字段包含 created_at、updated_at、order_no、total_amount。抽象基类的关键约束是:不能有外键指向抽象基类,因为它没有对应的表。若需要被外键引用,应改用多表继承或独立模型加外键。

二、多表继承:父表存公共字段,子表存专有字段

多表继承会为父模型和子模型各建一张表,子表通过 parent_ptr 外键关联父表。例如企业系统中“基础用户”与“员工”“客户”的关系:

class BaseUser(models.Model):
    username = models.CharField(max_length=64, unique=True)
    email = models.EmailField()

class Employee(BaseUser):
    employee_no = models.CharField(max_length=32, unique=True)
    department = models.CharField(max_length=64)

class Customer(BaseUser):
    level = models.CharField(max_length=16, default="normal")

查询 Employee.objects.all() 时,Django 会自动 JOIN 父表,返回包含父类字段的完整对象。代价是每次查询都有 JOIN 开销,且父表数据量会随所有子类增长。因此多表继承适合“子类数量少、公共字段多、需要统一查询父类”的场景;若子类差异极大,独立模型加一对一字段往往更清晰。

三、代理模型:改变行为而不改变表结构

代理模型不新建表,它复用原模型的表,只改变 Python 层的行为,例如默认排序、管理器或方法:

class Order(models.Model):
    order_no = models.CharField(max_length=32, unique=True)
    status = models.CharField(max_length=16)

class PaidOrder(Order):
    class Meta:
        proxy = True
        ordering = ["-order_no"]

    def mark_refunded(self):
        self.status = "refunded"
        self.save(update_fields=["status"])

PaidOrder 与 Order 操作同一张表,但拥有独立的默认排序和方法。代理模型常用于后台管理差异化展示、为同一张表提供不同业务视角,而不会引入额外迁移表。

四、迁移策略:先加后删、分步执行

模型继承一旦上线,修改结构必须考虑存量数据。企业环境推荐“扩展—迁移—收缩”三步法:先新增可空字段或新表,再写数据迁移回填,最后删除旧字段。下面这张图展示了抽象基类、多表继承、代理模型三者在数据库层面的差异,以及迁移时的安全路径。

模型继承方式与数据库表结构对照 抽象基类 abstract=True TimeStampedModel created_at / updated_at 不生成表,字段复制到子类 Order 表 含 created_at / updated_at 多表继承 BaseUser 表 username / email Employee 表 employee_no / department 子表通过 parent_ptr 外键关联 代理模型 proxy=True Order 表(唯一物理表) order_no / status PaidOrder(代理) 共享表,独立排序与方法 不产生新表,无额外迁移 企业级迁移策略:扩展 → 回填 → 收缩 ① 扩展 Expand 新增可空字段或新表 旧代码仍可运行 不破坏线上读写 ② 回填 Backfill RunPython 数据迁移 分批处理存量数据 可回滚、可重跑 ③ 收缩 Contract 删除旧字段或旧表 确认无代码引用后执行 保留回滚迁移文件

五、迁移中的常见陷阱与判断依据

第一,抽象基类改字段会同时影响所有子类,迁移文件会为每个子类生成 AddField,务必确认子类数量。第二,多表继承删除父类字段时,子类查询会立即报错,应先确认没有代码引用。第三,代理模型不能新增数据库字段,若确实需要新字段,必须改为多表继承或独立模型。第四,数据迁移中避免在 RunPython 里调用模型方法,应使用 apps.get_model 获取历史模型,防止模型演进后迁移失效。

判断继承方式的口诀:只共享字段和方法、不需要被外键引用,用抽象基类;需要统一查询父类且子类较少,用多表继承;只想改变默认排序或管理器、不新增字段,用代理模型。

最后给出一个可执行的迁移检查清单,用于上线前逐项确认:

检查项判断依据处理方式
新增字段是否可空表中有存量数据先 null=True,回填后再收紧
是否删除被引用字段存在外键或代码引用先改代码,再执行收缩迁移
数据迁移是否分批数据量超过十万行用 iterator() 分批更新
代理模型是否新增字段Meta 中 proxy=True禁止新增字段,改用多表继承
迁移是否可回滚存在 reverse_code为 RunPython 提供反向函数

把这套方法落到项目里,模型层就能在保持业务表达力的同时,让数据库迁移始终处于可控、可回滚的状态。