Django Signals(信号)监听模型或某一字段变化
欢迎访问我的博客查看 我的博客
Django信号
Django 提供一个“信号分发器”,允许解耦的应用在框架的其它地方发生操作时会被通知到。 简单来说,信号允许特定的sende
r通知一组receiver
某些操作已经发生。 这在多处代码和同一事件有关联的情况下很有用。
Django提供一组内建的信号,允许用户的代码获得Django特定操作的通知。 它们包含一些有用的通知:
django.db.models.signals.pre_save&django.db.models.signals.post_save
在模型 save()
方法调用之前或之后发送。
django.db.models.signals.pre_delete&django.db.models.signals.post_delete
在模型delete()
方法或查询集的delete() 方法调用之前或之后发送。
django.db.models.signals.m2m_changed
模型上的ManyToManyField
修改时发送。
django.core.signals.request_started&django.core.signals.request_finished
Django开始或完成HTTP请求时发送。
示例
示例分析:现有一篇博客,其中正文会包含很多图片,然而在我们创建文章时,没有事先创建好id的情况下,无法将这些图片关联到对应的文章,一旦当文章中的图片变动时,我们想从图片数据库、磁盘中删除这些图片,就会比较麻烦。所以引入信号,在发布的文章保存时,查找正文中用到的图片,将其关联到该文章。
当然可以改变一下思路,当用户点击创建博客时,通过ajax向后台提交一个创建博客的请求(那么model中需要设置其他字段可为为null才行),该请求会返回博客的id,进入编辑页面;当编辑博客中提交的图片,就把该id一并传入后台,关联到图片对象中;保存博客相当于就是更新该id对应的对象了。
模型数据库设计
下面用普通的方法,用信号实现。
models
class Article(models.Model):
title = models.CharField(max_length=100, verbose_name='标题')
author = models.ForeignKey(UserProfile, related_name='blog_articles', blank=True, null=True, on_delete=models.SET_NULL, verbose_name='作者')
content = models.TextField(blank=True, null=True, verbose_name='正文')
# 省略部分字段
def __str__(self):
return self.title
class Meta:
ordering = ['-publish_time', ] # 按照发布时间降序,旧的时间在后,也就是新发布的博客放在前面
verbose_name = '博客文章'
verbose_name_plural = '文章列表'
# 图片存储
class BlogImage(models.Model):
article = models.ForeignKey(Article, blank=True, null=True, on_delete=models.CASCADE, related_name='blog_images', verbose_name='关联文章')
title = models.CharField(max_length=50, null=True, blank=True, verbose_name='标题')
image = models.ImageField(upload_to='blog/images/%Y/%m', blank=True, null=True, verbose_name='图片')
class Meta:
verbose_name = '博客图集'
verbose_name_plural = verbose_name
def __str__(self):
return self.title
由于编辑博客中富文本或Markdown(我这使用)编辑器上传的图片基本都是ajax异步上传,该方式图片没有关联到文章
匹配正文中的图片字符串保存路径
写一个函数,用于正则匹配正文中的Markdown格式图片文本
# apps/blog/tools/manage_image_resources.py
from blog.models import BlogImage
def get_content_image_instance(article_instance):
"""
正则匹配文中的图片字符串,获取其中的本地位置,返回所有的与该文章相关的图片对象
:param article_instance:
:return:
"""
# 例如
# ![](/media/blog/images/2018/10/BLOG_20181008_221449_65.jpg)
# [![423423](/media/blog/images/2018/10/BLOG_20181008_221459_67.jpg "423423")](http://432 "423423")
# ![BLOG_20181008_221702_36](/media/blog/images/2018/10/BLOG_20181008_221702_36.png "博客图集BLOG_20181008_221702_36.png")
# [![](/media/blog/images/2018/10/BLOG_20181008_221953_62.jpg)](http://68768)
# ![BLOG_20181008_223948_94](/media/blog/images/2018/10/BLOG_20181008_223948_94.png "博客图集BLOG_20181008_223948_94.png")
results = re.findall(r'!\[(.*?)\]\(/media/(.*?)\)', article_instance.content)
blog_images = []
# print(results)
# for r in results:
# print(r)
# print(len(results))
for result in results:
# print(result[1])
# if ' ' in result[1]:
# image_url = result[1].split()[0] # 'blog/images/2018/10/BLOG_20181008_221702_36.png "博客图集BLOG_20181008_221702_36.png"'
# else:
# image_url = result[1] # 'blog/images/2018/10/BLOG_20181008_221449_65.jpg'
# print(image_url)
image = BlogImage.objects.filter(image=result[1].split()[0]) # 获取博客图片链接字符串
if image:
image = image.first() # 假定唯一
blog_images.append(image)
return blog_images
传入文章实例,获取正文中的图片对象。
创建信号处理函数,创建、更新文章执行
在应用下创建 signals.py 文件,用于放置处理函数
# apps/blog/signals.py
import hashlib
import os
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Article
from .tools.manage_image_resources import get_content_image_instance
@receiver(post_save, sender=Article)
def blog_save_handler(sender, instance=None, created=False, **kwargs):
"""
1、创建对象时,将id加密,保存到ecpid字段中,需要在应用/apps.py中进行重载
2、更新对象,获取正文中的图片资源,将不存在的进行删除;
:param sender:
:param instance: 创建的对象
:param created:
:param kwargs:
:return:
"""
if created:
# 加密文章对象的id,之后用这个字段访问文章
article_id = instance.id # instance指的就是创建的对象
obj_id = str(article_id)
md5 = hashlib.md5() # 生成一个MD5对象
md5.update(obj_id.encode('utf-8')) # 使用md5对象里的update方法md5转换
instance.ecpid = md5.hexdigest() # 得到加密后的字符串
instance.save()
# 获取该文章中用到过的图片,添加关联文章
images = get_content_image_instance(instance) # 获取该文章所有的图片对象
for image_instance in images:
image_instance.article = instance
image_instance.save()
else:
# created为False,更新操作
# 修改该文章中变动的图片信息
images = get_content_image_instance(instance) # 获取该文章所有的图片对象
old_images = instance.blog_images.all() # 图片数据库已存在的该文章所有图片对象
# 原不存在,新添加的有,增加
for image_instance in images:
if image_instance not in old_images:
image_instance.article = instance
image_instance.save()
# 原存在,新添加的无,删除
for image_instance in old_images:
if image_instance not in images: # 不在新提交的里面
image_path = image_instance.image.path
if os.path.exists(image_path):
print('图片存在,在磁盘中进行删除:', image_path)
os.remove(image_path)
# 删除数据库中的图库实例
image_instance.delete()
当创建博客post提交保存后,就会获取正文中的图片对象,添加文章的外键关联。
引入信号模块路径
编辑应用下的 __init__.py 添加
default_app_config = 'blog.apps.BlogConfig'
编辑 apps.py 重载BlogConfig
的ready
方法
from django.apps import AppConfig
class BlogConfig(AppConfig):
name = 'blog'
verbose_name = '个人博客'
def ready(self):
"""
在子类中重写此方法,以便在Django启动时运行代码。
:return:
"""
from .signals import blog_save_handler
但是,模型中任意字段变化都会被监听
在Article
模型中也有另外两个字段,浏览量、点赞数
class Article(models.Model):
# ...
views = models.PositiveIntegerField(default=0, verbose_name='浏览量')
likes = models.PositiveIntegerField(default=0, verbose_name='点赞数')
实际上通过以上设置,当用户浏览文章、或者是点赞,都会造成该信号处理函数被调用,那么如何去只监听某个字段的变化呢?
指定被监听的字段
模型信号并没有提供针对特定字段值变化的广播功能,虽然该信号提供了 update_fields
参数,但是并不能证明在该参数中的字段名的字段值一定发生了变化,所以我们要采用一个结合 post_init
信号的变通方法。
import hashlib
import os
from django.db.models.signals import post_save, post_init
from django.dispatch import receiver
from .models import Article
from .tools.manage_image_resources import get_content_image_instance
@receiver(post_init, sender=Article)
def blog_post_init(instance, **kwargs):
"""
缓存原始的值在 __original_name 中
:param instance:
:param kwargs:
:return:
"""
instance.__original_content = instance.content
@receiver(post_save, sender=Article)
def blog_save_handler(sender, instance=None, created=False, **kwargs):
"""
1、创建对象时,将id加密,保存到ecpid字段中,需要在应用/apps.py中进行重载
2、更新对象,获取正文中的图片资源,将不存在的进行删除;
:param sender:
:param instance: 创建的对象
:param created:
:param kwargs:
:return:
"""
if created:
# 加密文章对象的id,之后用这个字段访问文章
article_id = instance.id # instance指的就是创建的对象
obj_id = str(article_id)
md5 = hashlib.md5() # 生成一个MD5对象
md5.update(obj_id.encode('utf-8')) # 使用md5对象里的update方法md5转换
instance.ecpid = md5.hexdigest() # 得到加密后的字符串
instance.save()
# 获取该文章中用到过的图片,添加关联文章
images = get_content_image_instance(instance) # 获取该文章所有的图片对象
for image_instance in images:
image_instance.article = instance
image_instance.save()
else:
# created为False,更新操作,且当缓存的正文和当前提交的正文不同时,才进行正文中的图片提取
if instance.__original_content != instance.content:
# 修改该文章中变动的图片信息
images = get_content_image_instance(instance) # 获取该文章所有的图片对象
old_images = instance.blog_images.all() # 图片数据库已存在的该文章所有图片对象
# 原不存在,新添加的有,增加
for image_instance in images:
if image_instance not in old_images:
image_instance.article = instance
image_instance.save()
# 原存在,新添加的无,删除
for image_instance in old_images:
if image_instance not in images: # 不在新提交的里面
image_path = image_instance.image.path
if os.path.exists(image_path):
print('图片存在,在磁盘中进行删除:', image_path)
os.remove(image_path)
# 删除数据库中的图库实例
image_instance.delete()
简单的说就是在该模型广播 post_init
信号的时候,在模型对象中缓存当前的字段值;在模型广播 post_save
(或 pre_save
)的时候,比较该模型对象的当前的字段值与缓存的字段值,如果不相同则认为该字段值发生了变化。
以上示例就是,当博客的content
字段发生变化时,才进行文中图片字符获取。