您现在的位置是: 网站首页 >Django Django
Django Signals(信号)监听模型或某一字段变化
admin2019年5月21日 15:32 【Django 】 991人已围观
# 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** ```python 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格式图片文本 ```python # 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 文件,用于放置处理函数 ```python # 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 添加 ```python default_app_config = 'blog.apps.BlogConfig' ``` 编辑 apps.py 重载`BlogConfig`的`ready`方法 ```python from django.apps import AppConfig class BlogConfig(AppConfig): name = 'blog' verbose_name = '个人博客' def ready(self): """ 在子类中重写此方法,以便在Django启动时运行代码。 :return: """ from .signals import blog_save_handler ``` # 但是,模型中任意字段变化都会被监听 在`Article`模型中也有另外两个字段,浏览量、点赞数 ```python class Article(models.Model): # ... views = models.PositiveIntegerField(default=0, verbose_name='浏览量') likes = models.PositiveIntegerField(default=0, verbose_name='点赞数') ``` 实际上通过以上设置,当用户浏览文章、或者是点赞,都会造成该信号处理函数被调用,那么如何去只监听某个字段的变化呢? ## 指定被监听的字段 模型信号并没有提供针对特定字段值变化的广播功能,虽然该信号提供了 `update_fields` 参数,但是并不能证明在该参数中的字段名的字段值一定发生了变化,所以我们要采用一个结合 `post_init` 信号的变通方法。 ```python 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`字段发生变化时,才进行文中图片字符获取。
很赞哦! (2)
相关文章
文章交流
- emoji