之前在测试视图函数所使用的SQL语句时设置过一个中间件,现在我们来简单看看中间件的使用。
中间件主要应用在鉴权、限流、日志等场景,要避免业务逻辑和同步耗时I/O。
这是之前的查询某个操作执行的SQL语句。在项目启动时,中间件会完成初始化,其中get_response是一个可调用对象,是下一个逻辑处理。__call__就是核心处理逻辑,每个请求都会执行一次这个逻辑。在视图执行之前,清空查询记录。然后get_response执行下一个逻辑(可能是中间件或者视图函数),然后获取查询次数,打印查询语句。
# 中间件小工具
from django.db import connection, reset_queries
class QueryCountMiddleware:
# 用于查询某个操作执行的SQL语句
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
reset_queries() # 每次请求前清空SQL记录
response = self.get_response(request)
num_queries = len(connection.queries)
print(f"[SQL 查询次数] {request.path}: {num_queries}")
for q in connection.queries:
print(q['sql'], q['time'])
return response
唔,这个是不是和python的装饰器有点类似,不过中间件是全局的,一旦启动(在settings.py里面注册),每一个HTTP请求都会生效。而装饰器更加细粒度,只是关注我们感兴趣的某个视图函数。
信号
信号(Signals)是一种广播和订阅系统,用于框架不同部分之间实现解耦通信,简单来说就是不同app之间连接,但是不需要app之间有任何了解或者依赖。信号有四个核心部分:
- 信号-Signal:本身就是广播电台,一个中间。
- 发送方-Sender:触发事件,通常是一个模型类。
- 接收方-Receiver:当订阅的信号被发送,这个函数就会自动调用。
- 连接-Connecting:将接收方(函数)和某个信号(电台)连接起来,告诉Django,当发送方发送信号时候,请调用接收方函数。
举个例子,比如两个app,Django自带的用户认证app,而另一个app是个人写得用于存储用户头像等额外信息。创建了一个新的用户,如果不用信号只能去修改用户认证app,而如果有信号,只需要发送一个信号,个人写的应用就会收听信号自动处理。
一个简单的示例:
# profiles/signals.py
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from django.dispatch import receiver
from .models import Profile # 假设你已经定义了 Profile 模型
@receiver(post_save, sender=User) # 使用装饰器进行“连接”
def create_user_profile(sender, instance, created, **kwargs):
"""
当 User 实例被保存后,自动调用此函数
"""
# created 是一个布尔值,True 表示这是新创建的记录
if created:
Profile.objects.create(user=instance)
print(f"为新用户 {instance.username} 创建了 Profile!")
# 接收函数的参数解释:
# sender: 发送信号的模型类 (这里是 User)
# instance: 被保存的具体实例对象 (比如叫 'John' 的那个用户对象)
# created: 一个布尔值,如果是 True,表示是 INSERT (创建);如果是 False,表示是 UPDATE (更新)。
# **kwargs: 包含其他参数的字典。
注册信号处理器:
from django.apps import AppConfig
class ProfilesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'profiles'
def ready(self):
# 当应用准备就绪时,导入信号处理模块
# 这样 Django 就能发现 @receiver 装饰器并完成连接
import profiles.signals
setting里配置:
INSTALLED_APPS = [
# ...
'profiles.apps.ProfilesConfig', # 或者直接写 'profiles',但显式指定更好
# ...
]
常见信号:
- 模型信号 (Model Signals)
- pre_save / post_save:模型 save() 方法调用前后。
- pre_delete / post_delete:模型 delete() 方法或查询集 delete() 调用前后。
- pre_init / post_init:模型 __init__() 方法执行前后。
- m2m_changed:当 ManyToManyField 字段被修改时触发。
- 请求/响应信号 (Request/Response Signals)
- request_started / request_finished:Django 开始和结束处理一个 HTTP 请求时。
- got_request_exception:处理请求过程中出现异常时。
缓存
之前我们试过权限放入缓存里,效果还可以。不过或许可以进一步优化一下,我们每次在不同的页面跳转(例如点击权限管理再点击客户管理),因为每次都要数据展示,所以每次都会去访问一下SQL数据库,实际上如果不更改信息,只是单纯的页面浏览,并不需要每次都访问。
对于上述场景,有两种常见思路,一种是设置有效期,比如设置1h后缓存自动失效;另一种是主动通知作废,就是数据库变动及时删除缓存并更新。第一种简单粗暴,但是有效期设置太长,容易出现数据不一致的问题,而如果有效期设置太短,缓存命中率就低,频繁访问数据库。第二种实时性更高,最大程度保证了数据一致性,但是需要在每次写操作后增加一个删除缓存的动作。
考虑到我们的应用场景,设置有效期显然难以满足需求,还是第二种更有效。不过也要适当结合第一种,作为兜底策略,防止因为某些异常(比如删除缓存失败)导致脏数据永远留在缓存里。
不过也不能每次更新都直接往缓存里写,毕竟可能有时候也不需要更新后的数据,而且如果其他用户在疯狂修改,总不能每次都把所有信息往缓存里塞吧。所以只需要一个通知最好,比如给缓存的客户信息数据加个版本号,有其他用户增删改数据后,仅仅需要版本号+1,然后下次访问时候如果版本号对应的数据不存在,再去访问数据库就好了。正好可以利用上述的信号机制。【旧版本数据怎么处理?】
原有的客户列表视图函数代码如下:
class CustomersSet(viewsets.ModelViewSet):
serializer_class = CustomerSerializer
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ["name", "email", "phone", "address"]
ordering_fields = ["created_at", "updated_at", "name"]
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Customer.objects.filter(owner=self.request.user).select_related('owner')
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
……
我们需要重写查询的函数list,其他的函数不变。
def list(self, request, *args, **kwargs):
"""
重写 list 方法,加入缓存逻辑
"""
# 获取客户数据版本号
version_key = 'crm:customers:version'
version = cache.get(version_key, 1) # 默认版本为1
# 构造动态且唯一的缓存键
query_params_str = json.dumps(sorted(request.query_params.items()))
query_hash = hashlib.md5(query_params_str.encode('utf-8')).hexdigest()
cache_key = f"crm:customers:list:u{request.user.id}:v{version}:{query_hash}"
# 尝试从缓存获取数据
cached_response_data = cache.get(cache_key)
if cached_response_data:
# 缓存命中
return Response(cached_response_data)
# 缓存未命中,执行原始的查询和序列化逻辑
response = super().list(request, *args, **kwargs)
# 将返回结果的数据部分写入缓存
# 我们只缓存 response.data,因为 Response 对象本身不能被 pickle
cache.set(cache_key, response.data, timeout=3600) # 设置TTL为1h
return response
- request.query_params.items():获取当前HTTP请求中所有的查询参数,并将其转换为一个包含键值对元组(tuples)的列表。比如请求的URL是 /customer/customers/?status=active&ordering=-created_at,那么 request.query_params.items() 的结果大致会是 [(‘status’, ‘active’), (‘ordering’, ‘-created_at’)]。
- sorted():不过为啥要对获取的查询参数进行排序呢?HTTP请求中查询参数的顺序是不影响结果的。例如 ?a=1&b=2 和 ?b=2&a=1 是同一个请求。如果不排序,直接转换成字符串,会得到两个不同的字符串,导致缓存失效。排序后,无论原始顺序如何,总能得到一个相同的结果,确保了键的唯一性。
- json.dumps():将一个Python对象序列化成一个JSON格式的字符串。例如json.dumps([(‘ordering’, ‘-created_at’), (‘status’, ‘active’)]) 会生成字符串 ‘[[“ordering”, “-created_at”], [“status”, “active”]]’。
下面我们新建一个信号相关的文件customer/signals.py:
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.core.cache import cache
from .models import Customer
@receiver([post_save, post_delete], sender=Customer)
def update_customer_version(sender, instance, **kwargs):
"""
当 Customer 模型实例被保存或删除后,递增客户数据版本号
"""
version_key = 'crm:customers:version'
try:
cache.incr(version_key)
except ValueError: # 如果键不存在,Django的cache.incr会抛出ValueError
cache.set(version_key, 2) # 设为2,因为初始版本是1