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 操作同一张表,但拥有独立的默认排序和方法。代理模型常用于后台管理差异化展示、为同一张表提供不同业务视角,而不会引入额外迁移表。
四、迁移策略:先加后删、分步执行
模型继承一旦上线,修改结构必须考虑存量数据。企业环境推荐“扩展—迁移—收缩”三步法:先新增可空字段或新表,再写数据迁移回填,最后删除旧字段。下面这张图展示了抽象基类、多表继承、代理模型三者在数据库层面的差异,以及迁移时的安全路径。
五、迁移中的常见陷阱与判断依据
第一,抽象基类改字段会同时影响所有子类,迁移文件会为每个子类生成 AddField,务必确认子类数量。第二,多表继承删除父类字段时,子类查询会立即报错,应先确认没有代码引用。第三,代理模型不能新增数据库字段,若确实需要新字段,必须改为多表继承或独立模型。第四,数据迁移中避免在 RunPython 里调用模型方法,应使用 apps.get_model 获取历史模型,防止模型演进后迁移失效。
最后给出一个可执行的迁移检查清单,用于上线前逐项确认:
| 检查项 | 判断依据 | 处理方式 |
|---|---|---|
| 新增字段是否可空 | 表中有存量数据 | 先 null=True,回填后再收紧 |
| 是否删除被引用字段 | 存在外键或代码引用 | 先改代码,再执行收缩迁移 |
| 数据迁移是否分批 | 数据量超过十万行 | 用 iterator() 分批更新 |
| 代理模型是否新增字段 | Meta 中 proxy=True | 禁止新增字段,改用多表继承 |
| 迁移是否可回滚 | 存在 reverse_code | 为 RunPython 提供反向函数 |
把这套方法落到项目里,模型层就能在保持业务表达力的同时,让数据库迁移始终处于可控、可回滚的状态。