美文网首页Python Web开发学习
Django Signals(信号)监听模型或某一字段变化

Django Signals(信号)监听模型或某一字段变化

作者: 吾星喵 | 来源:发表于2019-05-21 15:36 被阅读2次

    欢迎访问我的博客查看 我的博客

    Django信号

    Django 提供一个“信号分发器”,允许解耦的应用在框架的其它地方发生操作时会被通知到。 简单来说,信号允许特定的sender通知一组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 重载BlogConfigready方法

    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字段发生变化时,才进行文中图片字符获取。

    相关文章

      网友评论

        本文标题:Django Signals(信号)监听模型或某一字段变化

        本文链接:https://www.haomeiwen.com/subject/tbsjzqtx.html