二、实战应用技巧— contentType与signals

contentType

一、contentType介绍

  在django中,有一个记录了项目中所有model元数据的表,就是ContentType,表中一条记录对应着一个存在的model,所以可以通过一个ContentType表的id和一个具体表中的id找到任何记录,及先通过ContenType表的id可以得到某个model,再通过model的id得到具体的对象。

class ContentType(models.Model):
    app_label = models.CharField(max_length=100)
    model = models.CharField(_('python model class name'), max_length=100)
    objects = ContentTypeManager()

    class Meta:
        verbose_name = _('content type')
        verbose_name_plural = _('content types')
        db_table = 'django_content_type'
        unique_together = (('app_label', 'model'),)

    def __str__(self):
        return self.name

  大家可以看到ContentType就是一个简单的django model,而且它在数据库中的表的名字为django_content_type。  这个类主要作用是记录每个app中的model。这个表的名字一般都不会陌生,在第一次对Django的model进行migrate之后,就可以发现在数据库中出现了一张默认生成的名为django_content_type的表。 
如果没有建立任何的model,默认django_content_type是这样的:

我们在自己的app中创建了如下几个model:post,event。迁移之后,我们来查看一下ContentType这个数据表中生成的数据:

  如上图,生成了app与model的对应关系。那么,這个主要有什么用呢?别急,听我慢慢道来。django_content_type记录了当前的Django项目中所有model所属的app(即app_label属性)以及model的名字(即model属性)。当然,django_content_type并不只是记录属性这么简单,contenttypes是对model的一次封装,因此可以通过contenttypes动态的访问model类型,而不需要每次import具体的model类型。

ContentType实例提供的接口 :

ContentType.model_class() #获取当前ContentType类型所代表的模型类

ContentType.get_object_for_this_type() #使用当前ContentType类型所代表的模型类做一次get查询ContentType管理器(manager)提供的接口 

ContentType.objects.get_for_id() #通过id寻找ContentType类型,这个跟传统的get方法的区别就是它跟get_for_model共享一个缓存,因此更为推荐。

ContentType.objects.get_for_model() #通过model或者model的实例来寻找ContentType类型

 我们在View视图中,来这样玩玩:

def demo(request):
    obj = models.ContentType.objects.get(id=10)
    print(obj.model_class()) # <class 'app01.models.Post'>
    return HttpResponse('............')

 二、contentType的用法

  那么这个表有什么作用呢?这里提供一个场景,网上商城购物时,会有各种各样的优惠券,比如通用优惠券,满减券,或者是仅限特定品类的优惠券。在数据库中,可以通过外键将优惠券和不同品类的商品表关联起来:

from django.db import models

class Electrics(models.Model):
    """
    id    name
     1   日立冰箱
     2   三星电视
     3   小天鹅洗衣机
    """
    name = models.CharField(max_length=32)

class Foods(models.Model):
    """
    id   name
    1    面包
    2    烤鸭
    """
    name = models.CharField(max_length=32)

class Clothes(models.Model):
    name = models.CharField(max_length=32)

class Coupon(models.Model):  # 特殊关系表
""" 
  id    name    electric_id   food_id   cloth_id   more...   # 每增加一张表,关系表的结构就要多加一个字段。
    1   通用优惠券   null       null      null 
    2   冰箱满减券   2         null     null 
    3   面包狂欢节   null        1      null 
""" 
    name = models.CharField(max_length=32) 
    electric = models.ForeignKey(to='Electrics', null=True) 
    food = models.ForeignKey(to='Foods', null=True) 
    cloth = models.ForeignKey(to='Clothes', null=True)

   如果是通用优惠券,那么所有的ForeignKey为null,如果仅限某些商品,那么对应商品ForeignKey记录该商品的id,不相关的记录为null。但是这样做是有问题的:实际中商品品类繁多,而且很可能还会持续增加,那么优惠券表中的外键将越来越多,但是每条记录仅使用其中的一个或某几个外键字段。
通过使用contenttypes 应用中提供的特殊字段GenericForeignKey,我们可以很好的解决这个问题。只需要以下三步:  

  • 在model中定义ForeignKey字段,并关联到ContentType表。通常这个字段命名为“content_type”
  • 在model中定义PositiveIntegerField字段,用来存储关联表中的主键。通常这个字段命名为“object_id”
  • 在model中定义GenericForeignKey字段,传入上述两个字段的名字。

为了更方便查询商品的优惠券,我们还可以在商品类中通过GenericRelation字段定义反向关系。

示例代码:

from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation

class Electrics(models.Model):
    name = models.CharField(max_length=32)
    price = models.IntegerField(default=100)
    coupons = GenericRelation(to='Coupon')  # 用于反向查询,不会生成表字段

    def __str__(self):
        return self.name

class Foods(models.Model):
    name = models.CharField(max_length=32)
    price=models.IntegerField(default=100)
    coupons = GenericRelation(to='Coupon')

    def __str__(self):
        return self.name


class Clothes(models.Model):
    name = models.CharField(max_length=32)
    price = models.IntegerField(default=100)
    coupons = GenericRelation(to='Coupon')

    def __str__(self):
        return self.name


class bed(models.Model):
    name = models.CharField(max_length=32)
    price = models.IntegerField(default=100)
    coupons = GenericRelation(to='Coupon')


class Coupon(models.Model):
    """
    Coupon
        id    name                      content_type_id       object_id_id
    美的满减优惠券            9(电器表electrics)  3
    猪蹄买一送一优惠券        10                    2
    南极被子买200减50优惠券   11                    1
    """
    name = models.CharField(max_length=32)

    content_type = models.ForeignKey(to=ContentType) # step 1
    object_id = models.PositiveIntegerField() # step 2
    content_object = GenericForeignKey('content_type', 'object_id') # step 3

    def __str__(self):
        return self.name

 注意:ContentType只运用于1对多的关系!!!并且多的那张表中有多个ForeignKey字段。

 创建记录和查询:

from django.shortcuts import render, HttpResponse
from api import models
from django.contrib.contenttypes.models import ContentType

def test(request):
    if request.method == 'GET':
        # ContentType表对象有model_class() 方法,取到对应model
        content = ContentType.objects.filter(app_label='api', model='electrics').first()  # 表名小写
        cloth_class = content.model_class() # cloth_class 就相当于models.Electrics
        res = cloth_class.objects.all()
        print(res)

        # 为三星电视(id=2)创建一条优惠记录
        s_tv = models.Electrics.objects.filter(id=2).first()
        models.Coupon.objects.create(name='电视优惠券', content_object=s_tv)

        # 查询优惠券(id=1)绑定了哪个商品
        coupon_obj = models.Coupon.objects.filter(id=1).first()
        prod = coupon_obj.content_object
        print(prod)

        # 查询三星电视(id=2)的所有优惠券
        res = s_tv.coupons.all()
        print(res)

        # 查询obj的所有优惠券:如果没有定义反向查询字段,通过如下方式:
        content = ContentType.objects.filter(app_label='api', model='model_name').first()
        res = models.OftenAskedQuestion.objects.filter(content_type=content, object_id=obj.pk).all()

        return HttpResponse('....')

 当一张表和多个表FK关联,并且多个FK中只能选择其中一个或其中n个时,可以利用contenttypes app,只需定义三个字段就搞定!

signals

一、signals介绍

  Django内部包含了一位“信号调度员”:当某事件在框架内发生时,它可以通知到我们的应用程序。 简而言之,当event(事件)发生时,signals(信号)允许若干 senders(寄件人)通知一组 receivers(接收者)。这在我们多个独立的应用代码对同一事件的发生都感兴趣时,特别有用。

  个人理解,django的signal可理解为django内部的钩子,当一个事件发生时,其他程序可对其作出相关反应,可通过signal来回调定义好的处理函数(receivers),从而更大程度的解耦我们的系统。

二、使用场景

通知类 

通知是signal最常用的场景之一。例如,在论坛中,在帖子得到回复时,通知楼主。从技术上来讲,我们可以将通知逻辑放在回复保存时,但是这并不是一个好的处理方式,这样会时程序耦合度增大,不利于系统的后期扩展维护。如果我们在回复保存时,只发一个简单的信号,外部的通知逻辑拿到信号后,再发送通知,这样回复的逻辑和通知的逻辑做到了分开,后期维护扩展都比较容易。

初始化类

信号的另一个列子便是事件完成后,做一系列的初始化工作。

其他一些使用场景总结

以下情况不要使用signal:

  • signal与一个model紧密相关, 并能移到该model的save()时

  • signal能使用model manager代替时

  • signal与一个view紧密相关, 并能移到该view中时

以下情况可以使用signal:

  • signal的receiver需要同时修改对多个model时

  • 将多个app的相同signal引到同一receiver中处理时

  • 在某一model保存之后将cache清除时

  • 无法使用其他方法, 但需要一个被调函数来处理某些问题时

三、signals的使用

信号系统包含以下三要素:

  • 信号:Signal对象
  • 发送者:信号发出方
  • 接收者:信号接收方

Django内置了一整套信号,下面是一些比较常用的:

  • pre_save & post_save    在ORM模型的save()方法调用之前或之后发送信号
  • pre_delete & post_delete    在ORM模型或查询集的delete()方法调用之前或之后发送信号
  • request_started & request_finished    当接收和关闭HTTP请求时发送信号
  • m2m_changed    当多对多字段被修改时发送信号

以上可以看出,在某种程度上说,信号和数据库触发器有些相似。

接下来看看Django信号如何使用:

from django.dispatch import receiver
from django.core.signals import request_finished
 
## decorators 方式绑定
@receiver(request_finished, dispatch_uid="request_finished")
def my_signal_handler(sender, **kwargs):
    print("Request finished!================================")

# 普通绑定方式
def my_signal_handler(sender, **kwargs):
    print("Request finished!================================")

request_finished.connect(my_signal_handler)

#####################################################
# 针对model 的signal 
from django.dispatch import receiver
from django.db.models.signals import post_save
 
from polls.models import MyModel
 
 
@receiver(post_save, sender=MyModel, dispatch_uid="mymodel_post_save")
def my_model_handler(sender, **kwargs):
 print('Saved: {}'.format(kwargs['instance'].__dict__))

# dispatch_uid 确保此receiver 只调用一次

ContentType与signals的联合使用

  django的signal结合contenttypes可以实现好友最新动态,新鲜事,消息通知等功能。总体来说这个功能就是在用户发生某个动作的时候将其记录下来或者附加某些操作,比如通知好友。要实现这种功能可以在动作发生的代码里实现也可以通过数据库触发器等实现,但在django中,一个很简单的方法的就是使用signals。

  当django保存一个object的时候会发出一系列的signals,可以通过对这些signals注册listener,从而在相应的signals发出时执行一定的代码。
  使用signals来监听用户的动作有很多好处,1、不管这个动作是发生在什么页面,甚至在很多页面都可以发生这个动作,都只需要写一次代码来监听保存object这个动作就可以了。2、可以完全不修改原来的代码就可以添加监听signals的功能。3、你几乎可以在signals监听代码里写任何代码,包括做一些判断是不是第一次发生此动作还是一个修改行为等等。

  想要记录下每个操作,同时还能追踪到这个操作的具体动作。
  *首先用信号机制,监听信号,实现对信号的响应函数,在响应函数中记录发生的动作(记录在一张记录表,相当于下文的Event)。
  *其次就是为了能追踪到操作的具体动作,必须从这张表中得到相应操作的model,这就得用到上面说的ContentType。

  对于新鲜事这个功能来说就是使用GenericRelation来产生一个特殊的外键,它不像models.ForeignKey那样,必须指定一个Model来作为它指向的对象。GenericRelation可以指向任何Model对象,有点像C语言中 void* 指针。

  这样关于保存用户所产生的这个动作,比如用户写了一片日志,我们就可以使用Generic relations来指向某个Model实例比如Post,而那个Post实例才真正保存着关于用户动作的完整信息,即Post实例本身就是保存动作信息最好的地方。这样我们就可以通过存取Post实例里面的字段来描述用户的那个动作了,需要什么信息就往那里面去取。而且使用Generic relations的另外一个好处就是在删除了Post实例后,相应的新鲜事实例也会自动删除。

  怎么从这张操作记录表中得到相应操作的model呢,这就得用到fields.GenericForeignKey,它是一个特殊的外键,可以指向任何Model的实例,在这里就可以通过这个字段来指向类似Post这样保存着用户动作信息的Model实例。

  先来看看model:

from django.db import models
from django.contrib.auth.models import User
from django.contrib.contenttypes import fields
from django.db.models import signals


class Post(models.Model):
    author = models.ForeignKey(User)
    title = models.CharField(max_length=255)
    content = models.TextField()
    created = models.DateTimeField(u'发表时间', auto_now_add=True)
    updated = models.DateTimeField(u'最后修改时间', auto_now=True)

    events = fields.GenericRelation('Event')

    def __str__(self):
        return self.title

    def description(self):
        return u'%s 发表了日志《%s》' % (self.author, self.title)


class Event(models.Model):
    user = models.ForeignKey(User)
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()

    content_object= fields.GenericForeignKey('content_type', 'object_id')

    created = models.DateTimeField(u'事件发生时间', auto_now_add=True)

    def __str__(self):
        return "%s的事件: %s" % (self.user, self.description())

    def description(self):
        return self.content_object.description()

def post_post_save(sender, instance, signal, *args, **kwargs):
    """
    :param sender:监测的类:Post类
    :param instance: 监测的类:Post类
    :param signal: 信号类
    :param args: 
    :param kwargs: 
    :return: 
    """
    post = instance
    event = Event(user=post.author, content_object=post)
    event.save()

signals.post_save.connect(post_post_save, sender=Post)
#signals.post_save.connect(post_post_sace,sender=Book)可以监听多个类

   只要model中有object的保存操作,都将执行post_post_save函数,故可以在这个接受函数中实现通知好友等功能。

  前面说到django在保存一个object的时候会发出一系列signals,在这里我们所监听的是signals.post_save这个signal,这个signal是在django保存完一个对象后发出的,django中已定义好得一些signal, 在django/db/models/signal.py中可以查看,同时也可以自定义信号。 
  利用connect这个函数来注册监听器, connect原型为:
  def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
  第一个参数是要执行的函数,第二个参数是指定发送信号的Class,这里指定为Post这个Model,对其他Model所发出的signal并不会执行注册的函数。
instance这个参数,即刚刚保存完的Model对象实例。创建事件的时候看到可以将post这个instance直接赋给generic.GenericForeignKey类型的字段,从而event实例就可以通过它来获取事件的真正信息了。
  最后有一点需要的注意的是,Post的Model定义里现在多了一个字段:
      content_object= GenericRelation(‘Event’)

      通过这个字段可以得到与某篇post相关联的所有事件,最重要的一点是如果没有这个字段,那么当删除一篇post的时候,与该post关联的事件是不会自动删除的。反之有这个字段就会进行自动的级联删除

原文地址:https://www.cnblogs.com/kxsph/p/11287786.html