Django 模型钩子函数是模型生命周期中的关键控制点,允许您在特定事件发生时执行自定义逻辑。本教程将全面解析 Django 模型中的钩子函数,帮助您掌握这些强大的工具。
一、模型生命周期概览
二、核心钩子函数详解
1. __init__
- 实例初始化钩子
触发时机:创建模型实例时(Product()
或 Product.objects.create()
)
使用场景:设置初始状态、缓存原始值、添加临时属性
最佳实践:
class Product(models.Model):
name = models.CharField(max_length=100)
price = models.DecimalField(max_digits=10, decimal_places=2)
def __init__(self, *args, **kwargs):
# 必须先调用父类构造函数
super().__init__(*args, **kwargs)
# 缓存原始价格用于后续比较
self._original_price = self.price
# 添加临时属性
self.creation_time = timezone.now()
2. save()
- 保存方法钩子
触发时机:调用 save()
方法时
使用场景:字段预处理、状态变更记录、自定义保存逻辑
最佳实践:
def save(self, *args, **kwargs):
"""保存前调用"""
if not self.slug:
self.slug = slugify(self.name)
if self.price < 0:
raise ValidationError("价格不能为负")
# 调用父类保存方法
super().save(*args, **kwargs)
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
"""保存后调用"""
# 创建库存记录
Stock.objects.create(product=self, quantity=0)
print(f"产品 {self.name} 已保存")
3. delete()
- 删除方法钩子
触发时机:调用 delete()
方法时
使用场景:删除保护、级联删除、备份数据
最佳实践:
def delete(self, *args, **kwargs):
"""删除前处理"""
# 检查关联订单
if self.order_set.exists():
raise ValidationError("无法删除有订单关联的产品")
# 创建备份
DeletedProduct.objects.create(
name=self.name,
price=self.price,
deleted_at=timezone.now()
)
# 必须调用父类删除方法
super().delete(*args, **kwargs)
三、信号系统深度解析
1. 信号是什么?
信号是 Django 的事件通知系统,允许解耦的组件在特定动作发生时执行操作。
2. 信号核心概念
3. pre_save
信号
触发时机:save()
方法开始执行,数据库操作前
使用场景:全局字段预处理、审计日志
最佳实践:
from django.db.models.signals import pre_save
from django.dispatch import receiver
# 注册信号接收器
@receiver(pre_save, sender=Product)
def product_pre_save_handler(sender, instance, **kwargs):
"""
sender: 发送信号的模型类 (Product)
instance: 实际保存的实例
"""
# 自动生成SKU
if not instance.sku:
instance.sku = generate_sku(instance.name)
# 记录修改人
if hasattr(instance, 'request') and instance.request.user:
instance.last_modified_by = instance.request.user
4. post_save
信号
触发时机:save()
方法完成,数据库操作后
使用场景:创建关联对象、发送通知、更新缓存
最佳实践:
@receiver(post_save, sender=Product)
def product_post_save_handler(sender, instance, created, **kwargs):
"""
created: 是否是新建对象
"""
if created:
# 初始化库存
Stock.objects.create(product=instance, quantity=0)
print(f"新产品创建: {instance.name}")
else:
# 发送更新通知
send_product_update_notification(instance)
5. pre_delete
信号
触发时机:delete()
方法开始执行,数据库操作前
使用场景:删除保护、备份数据
最佳实践:
@receiver(pre_delete, sender=Product)
def product_pre_delete_handler(sender, instance, **kwargs):
# 检查是否有未完成订单
if Order.objects.filter(product=instance, status='pending').exists():
raise ValidationError("无法删除有未完成订单的产品")
# 创建备份
ProductArchive.objects.create(
name=instance.name,
price=instance.price,
deleted_at=timezone.now()
)
6. post_delete
信号
触发时机:delete()
方法完成,数据库操作后
使用场景:清理关联数据、更新统计信息
最佳实践:
@receiver(post_delete, sender=Product)
def product_post_delete_handler(sender, instance, **kwargs):
# 清理图片文件
if instance.image:
instance.image.delete(save=False)
# 更新分类统计
category = instance.category
category.product_count = Product.objects.filter(category=category).count()
category.save()
四、验证钩子函数
1. 字段级验证
触发时机:调用 full_clean()
或表单验证时
使用场景:复杂业务规则验证、跨字段验证
最佳实践:
def clean(self):
"""模型级验证"""
errors = {}
# 验证价格
if self.price < self.cost_price:
errors['price'] = "售价不能低于成本价"
# 验证库存
if self.stock < 0:
errors['stock'] = "库存不能为负"
if errors:
raise ValidationError(errors)
2. 唯一性验证
def validate_unique(self, exclude=None):
"""自定义唯一性验证"""
# 检查名称唯一性(忽略大小写)
if Product.objects.filter(name__iexact=self.name).exclude(pk=self.pk).exists():
raise ValidationError({'name': '产品名称已存在'})
super().validate_unique(exclude)
五、高级钩子技巧
1. 状态转换钩子
class Order(models.Model):
STATUS_CHOICES = [('P', '待付款'), ('S', '已发货'), ('C', '已完成')]
status = models.CharField(max_length=1, choices=STATUS_CHOICES, default='P')
def save(self, *args, **kwargs):
# 记录状态变更
if self.pk:
original = Order.objects.get(pk=self.pk)
if original.status != self.status:
OrderStatusLog.objects.create(
order=self,
from_status=original.status,
to_status=self.status
)
super().save(*args, **kwargs)
2. 审计日志钩子
@receiver(post_save, sender=Product)
@receiver(post_delete, sender=Product)
def product_audit_log(sender, instance, **kwargs):
"""产品变更审计日志"""
action = 'CREATE' if kwargs.get('created') else 'UPDATE'
if kwargs.get('signal') == post_delete:
action = 'DELETE'
AuditLog.objects.create(
model='Product',
object_id=instance.pk,
action=action,
details={
'name': instance.name,
'price': instance.price
}
)
3. 缓存管理钩子
@receiver(post_save, sender=Product)
@receiver(post_delete, sender=Product)
def clear_product_cache(sender, **kwargs):
"""清除产品缓存"""
from django.core.cache import cache
cache.delete('featured_products')
cache.delete('product_categories')
cache.delete_pattern('product_detail_*')
六、最佳实践指南
1. 钩子使用原则
钩子类型 | 适用场景 | 不适用场景 |
---|---|---|
模型方法 | 模型特定逻辑 | 跨模型操作 |
信号 | 全局事件处理 | 简单字段转换 |
验证方法 | 复杂业务规则 | 基本格式验证 |
2. 性能优化技巧
# 避免在钩子中进行耗时操作
@receiver(post_save, sender=Order)
def process_order(sender, instance, **kwargs):
# 使用异步任务处理耗时操作
from .tasks import process_order_async
process_order_async.delay(instance.id)
3. 错误处理
def save(self, *args, **kwargs):
try:
# 业务逻辑
self.clean()
super().save(*args, **kwargs)
except ValidationError as e:
# 处理验证错误
logger.error(f"保存失败: {e}")
raise
except Exception as e:
# 处理其他错误
logger.exception("未知错误")
raise
七、实战案例:电商产品模型
from django.db import models
from django.db.models.signals import pre_save, post_save, pre_delete
from django.dispatch import receiver
from django.core.exceptions import ValidationError
from django.utils.text import slugify
from django.utils import timezone
class Product(models.Model):
name = models.CharField(max_length=200, unique=True)
slug = models.SlugField(max_length=200, blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
cost_price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._original_price = self.price
def clean(self):
"""验证业务规则"""
if self.price < self.cost_price:
raise ValidationError("售价不能低于成本价")
if self.stock < 0:
raise ValidationError("库存不能为负")
def save(self, *args, **kwargs):
"""保存前处理"""
# 自动生成slug
if not self.slug:
self.slug = slugify(self.name)
# 检查价格变化
if self.pk and self.price != self._original_price:
PriceChange.objects.create(
product=self,
old_price=self._original_price,
new_price=self.price
)
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
"""删除前处理"""
if self.stock > 0:
raise ValidationError("库存不为零的产品不能删除")
# 创建备份
DeletedProduct.objects.create(
name=self.name,
price=self.price,
deleted_at=timezone.now()
)
super().delete(*args, **kwargs)
# 信号处理
@receiver(pre_save, sender=Product)
def product_pre_save(sender, instance, **kwargs):
"""全局SKU生成"""
if not instance.sku:
instance.sku = generate_unique_sku()
@receiver(post_save, sender=Product)
def product_post_save(sender, instance, created, **kwargs):
"""库存初始化"""
if created:
Stock.objects.get_or_create(product=instance, defaults={'quantity': 0})
@receiver(pre_delete, sender=Product)
def product_pre_delete(sender, instance, **kwargs):
"""删除前检查关联"""
if OrderItem.objects.filter(product=instance).exists():
raise ValidationError("有订单关联的产品不能删除")
八、单元测试钩子函数
from django.test import TestCase
from .models import Product
class ProductHooksTest(TestCase):
def test_slug_auto_generation(self):
"""测试slug自动生成"""
p = Product.objects.create(name="Premium Coffee", price=9.99)
self.assertEqual(p.slug, "premium-coffee")
def test_price_change_log(self):
"""测试价格变更日志"""
p = Product.objects.create(name="Tea", price=5.99)
p.price = 6.99
p.save()
self.assertEqual(PriceChange.objects.count(), 1)
self.assertEqual(PriceChange.objects.first().new_price, 6.99)
def test_delete_protection(self):
"""测试删除保护"""
p = Product.objects.create(name="Milk", price=3.49, stock=10)
with self.assertRaises(ValidationError):
p.delete()
总结与最佳实践
核心钩子函数
钩子 | 方法/信号 | 最佳用途 |
---|---|---|
初始化 | __init__ | 设置初始状态 |
保存前 | pre_save 信号 | 字段预处理 |
保存中 | save() 方法 | 模型特定逻辑 |
保存后 | post_save 信号 | 关联操作 |
删除前 | pre_delete 信号 | 删除保护 |
删除中 | delete() 方法 | 模型特定清理 |
删除后 | post_delete 信号 | 清理关联 |
验证 | clean() 方法 | 业务规则验证 |
黄金法则
- 保持钩子精简:避免复杂逻辑
- 使用异步任务:处理耗时操作
- 信号解耦:跨模型操作使用信号
- 错误处理:添加异常捕获
- 测试覆盖:为所有钩子编写测试
性能口诀
“钩子轻量效率高,
异步处理是法宝,
信号解耦复用好,
验证逻辑不能少。”
通过掌握这些钩子函数,您可以:
- 实现复杂的业务逻辑
- 自动维护数据一致性
- 增强数据完整性保护
- 构建审计和日志系统
- 优化应用性能