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。
四、落地要点与常见陷阱
第一,软删除字段必须加索引,否则 objects 的默认过滤会拖慢查询。第二,all_objects 不要暴露给普通业务代码,避免绕过业务规则。第三,审计日志的 before/after 建议只记录变更字段,而不是整行快照,否则数据量会迅速膨胀。第四,自定义字段的 deconstruct 要谨慎处理,确保迁移可逆。第五,Manager 的 get_queryset 一旦过滤,update、delete 也会受影响,需确认这是预期行为。
SoftDeleteModel 作为抽象基类,所有需要软删除的模型继承它;把 AuditLog 的写入封装成 record_audit(instance, action, operator, before, after) 工具函数,在视图、序列化器或信号中统一调用,避免审计逻辑散落各处。综合来看,自定义字段解决“数据长什么样”,自定义 Manager/QuerySet 解决“怎么查”,软删除与审计解决“删了之后还能不能追溯”。三者组合,才能让模型层真正承载企业级业务规则,而不是把约束散落在视图与表单中。