4. 自定义模型字段与管理器

在Django企业项目中,模型层往往承载着最核心的业务规则。默认的 CharField、IntegerField 与 objects 管理器只能满足通用需求,一旦遇到“金额必须精确到分且带币种”“手机号必须符合中国大陆格式”“删除只能逻辑删除并留痕”这类要求,就需要把业务约束下沉到字段、Manager 与 QuerySet 中。本节围绕三个可落地的能力展开:业务专用字段、自定义 Manager/QuerySet、软删除与数据审计。

一、业务专用字段:把校验与转换封装进字段

自定义字段的价值在于“一次定义,处处复用”。以金额字段为例,企业系统通常要求:数据库用 DECIMAL(12,2) 存储、Python 侧用 Decimal 运算、对外序列化时保留两位小数。我们可以继承 DecimalField 并固定参数:

from decimal import Decimal
from django.db import models

class MoneyField(models.DecimalField):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault("max_digits", 12)
        kwargs.setdefault("decimal_places", 2)
        super().__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        kwargs.pop("max_digits", None)
        kwargs.pop("decimal_places", None)
        return name, path, args, kwargs

重写 deconstruct 是为了让迁移文件不重复记录固定参数,避免后续修改默认值时产生无意义的迁移。若字段还涉及“币种”这一伴随信息,更推荐用组合字段或 MoneyField + currency 字段的方式,而不是把币种塞进字符串。

再如手机号字段,可在 validators 中挂载正则校验,并在 to_python 中做规范化(去空格、去 +86 前缀):

import re
from django.core.validators import RegexValidator

PHONE_RE = RegexValidator(
    r"^1[3-9]\d{9}$", "请输入有效的中国大陆手机号"
)

class PhoneField(models.CharField):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault("max_length", 11)
        kwargs.setdefault("validators", [PHONE_RE])
        super().__init__(*args, **kwargs)

    def to_python(self, value):
        if value is None:
            return value
        return re.sub(r"[\s\-+]|^86", "", str(value))

这样,模型层、表单层、序列化层都能共享同一套规则,减少“每个入口各写一遍校验”的重复与遗漏。

二、自定义 Manager 与 QuerySet:让查询语义化

Manager 是模型访问数据库的入口,QuerySet 是可链式调用的查询集合。企业项目里常见的做法是:把“常用过滤条件”写成 QuerySet 方法,再由 Manager 暴露出去。例如订单模型:

class OrderQuerySet(models.QuerySet):
    def paid(self):
        return self.filter(status="paid")

    def recent(self, days=7):
        from django.utils import timezone
        from datetime import timedelta
        since = timezone.now() - timedelta(days=days)
        return self.filter(created_at__gte=since)

    def with_customer(self):
        return self.select_related("customer")

class OrderManager(models.Manager.from_queryset(OrderQuerySet)):
    pass

class Order(models.Model):
    status = models.CharField(max_length=16, default="created")
    created_at = models.DateTimeField(auto_now_add=True)
    customer = models.ForeignKey("Customer", on_delete=models.PROTECT)
    objects = OrderManager()

调用时即可写成 Order.objects.paid().recent(30).with_customer(),语义清晰且可复用。需要注意:from_queryset 生成的管理器会同时具备 QuerySet 方法,避免在 Manager 中重复定义。若同一模型需要多个入口,可再定义 paid_orders = OrderManager().filter(status="paid") 之类的专用管理器,但不要滥用,否则会掩盖真实查询条件。

三、软删除与数据审计:删除不是消失,而是状态迁移

软删除的核心是“用字段标记删除状态,而不是物理删除行”。典型做法是在模型上加 is_deleted、deleted_at、deleted_by 三个字段,并让默认 Manager 只返回未删除数据,同时保留一个 all_objects 用于后台审计与恢复。

class SoftDeleteQuerySet(models.QuerySet):
    def delete(self):
        return super().update(is_deleted=True, deleted_at=timezone.now())

    def hard_delete(self):
        return super().delete()

    def alive(self):
        return self.filter(is_deleted=False)

class SoftDeleteManager(models.Manager.from_queryset(SoftDeleteQuerySet)):
    def get_queryset(self):
        return super().get_queryset().filter(is_deleted=False)

class SoftDeleteModel(models.Model):
    is_deleted = models.BooleanField(default=False, db_index=True)
    deleted_at = models.DateTimeField(null=True, blank=True)
    deleted_by = models.CharField(max_length=64, blank=True)

    objects = SoftDeleteManager()
    all_objects = models.Manager()

    class Meta:
        abstract = True

这里有一个关键点:objects 默认过滤掉已删除数据,all_objects 保留全量。业务代码用 objects,后台审计、数据恢复、统计报表用 all_objects。删除时调用 obj.delete() 会走 QuerySet 的 update,不会触发 post_delete 信号,因此审计日志需要单独记录。

数据审计通常包含“谁、何时、对哪条记录、做了什么、改前改后是什么”。可以在模型层用信号或 save 重写实现,但更推荐把审计写入独立表,避免污染业务表:

class AuditLog(models.Model):
    model_name = models.CharField(max_length=64)
    object_id = models.CharField(max_length=64)
    action = models.CharField(max_length=16)  # create/update/delete
    operator = models.CharField(max_length=64)
    before = models.JSONField(default=dict)
    after = models.JSONField(default=dict)
    created_at = models.DateTimeField(auto_now_add=True)

在 save 中对比新旧值,或在 pre_save/post_save 信号中记录差异。注意:软删除的“删除”动作也应写入审计,action="delete",after 中记录 is_deleted=True 与 deleted_at。

软删除与审计的数据流 业务请求 Order.objects SoftDeleteManager 过滤 is_deleted=False 业务表 含 is_deleted 字段 AuditLog before / after 删除操作 obj.delete() UPDATE 而非 DELETE is_deleted=True 记录保留 deleted_at / by 写入审计 action=delete 恢复操作 all_objects UPDATE 回滚 is_deleted=False 数据可见 objects 重新返回 审计追加 action=restore

四、落地要点与常见陷阱

第一,软删除字段必须加索引,否则 objects 的默认过滤会拖慢查询。第二,all_objects 不要暴露给普通业务代码,避免绕过业务规则。第三,审计日志的 before/after 建议只记录变更字段,而不是整行快照,否则数据量会迅速膨胀。第四,自定义字段的 deconstruct 要谨慎处理,确保迁移可逆。第五,Manager 的 get_queryset 一旦过滤,update、delete 也会受影响,需确认这是预期行为。

建议把 SoftDeleteModel 作为抽象基类,所有需要软删除的模型继承它;把 AuditLog 的写入封装成 record_audit(instance, action, operator, before, after) 工具函数,在视图、序列化器或信号中统一调用,避免审计逻辑散落各处。

综合来看,自定义字段解决“数据长什么样”,自定义 Manager/QuerySet 解决“怎么查”,软删除与审计解决“删了之后还能不能追溯”。三者组合,才能让模型层真正承载企业级业务规则,而不是把约束散落在视图与表单中。