在《发现一个好东西: Celery 分布式的任务队列》 中 我们使用Celery 来实现一个异步的数据处理过程,加上使用rabbitmq则可以实现一个强大的大数据异步数据框架。
但是在小应用中,如何使用Django快速开发呢?
一般情况下,我们使用signal就可以实现一个简单的异步的数据处理过程。例如在下面的应用场景中,
- sns中的事件通知, 如用户发表了一篇博文, 然后通知所有的好友
- 用户信息的更改的邮件通知
- 用户订制信息的邮件通知等
我们都是可以使用signal功能来实现的。
前言
看到很多SNS网站上,像校内,5G都有一个很棒的功能,就是登录之后在自己的首页,可以看到自己好友最新发生的动态。于是想到使用django其实可以非常简单的实现这个功能,并且效果比现在SNS网站所用的更好。
总体来说这个功能就是在用户发生某个动作的时候将其记录下来,我不知道别人是怎么实现的,也许是直接在发生那个动作的代码块里写死,也许是使用数据库的触发器之类,但是在django中,一个很简单的方法的就是使用signals。
什么是signals:
最初看到signals是在django的The database API文档,当保存一个object的时候,会发生下面的这些事情(原文):
What happens when you save?
When you save an object, Django performs the following steps:
- Emit a
pre_save
signal. This provides a notification that an object is about to be saved. You can register a listener that will be invoked whenever this signal is emitted. (These signals are not yet documented.)
Pre-process the data. Each field on the object is asked to perform any automated data modification that the field may need to perform.
- Most fields do no pre-processing — the field data is kept as-is. Pre-processing is only used on fields that have special behavior. For example, if your model has a DateField with auto_now=True, the pre-save phase will alter the data in the object to ensure that the date field contains the current date stamp. (Our documentation doesn’t yet include a list of all the fields with this “special behavior.”)
- Prepare the data for the database. Each field is asked to provide its current value in a data type that can be written to the database.
Most fields require no data preparation. Simple data types, such as integers and strings, are ‘ready to write’ as a Python object. However, more complex data types often require some modification.
For example, DateFields use a Python datetime object to store data. Databases don’t store datetime objects, so the field value must be converted into an ISO-compliant date string for insertion into the database.
- Insert the data into the database. The pre-processed, prepared data is then composed into an SQL statement for insertion into the database.
- Emit a
post_save
signal. As with the pre_save signal, this is used to provide notification that an object has been successfully saved. (These signals are not yet documented.)
简单来说就是当django保存一个object的时候会发出一系列的signals,可以通过对这些signals注册listener,从而在相应的signals发出时执行一定的代码。关于signals的文档django还没有整理出来,不过找了些资料来看了看,不算很难懂,基本使用还是很简单的。
使用signals来监听用户的动作有很多好处,
- 不管这个动作是发生在什么页面,甚至在很多页面都可以发生这个动作,都只需要写一次代码来监听保存object这个动作就可以了。
- 可以完全不修改原来的代码就可以添加监听signals的功能。
- 你几乎可以在signals监听代码里写任何代码,包括做一些判断是不是第一次发生此动作还是一个修改行为等等。
鉴于本人表达能力有限,如果看到这里感觉稀里糊涂,可以考虑先看一下本文底部所列出的一些资料获取更多信息。
现在需要面对的就是第二个问题,监听到用户动作之后如何完整有效地保存用户这一动作,保存一个字符串来描述这个动作?通过外键来指向某个表中的某条记录?
保存一个字符串来描述这个动作是一个很高效的方法,但是缺乏灵活性,比如用户发表了一篇日志,但是很快又删了,如何把这个多余的新鲜事记录找出来同时删除?想修改已经保存了的字符串描述该怎么办?
而通过外键来指向某个表中的某条记录虽然可以解决删除问题,但是需要为不同类型的动作各自添加一张表来对应,如果以后有新的功能实现需要添加新表,如何可以简单来进行扩展呢?
使用contenttypes framework
而这些django的 contenttypes framework就可以很好的解决。
什么是contenttypes framework(原文):
Django includes a “contenttypes” application that can track all of the models installed in your Django-powered project, providing a high-level, generic interface for working with your models.
这句话听上去很难理解,不过对于新鲜事这个功能来说就是使用Generic relations来产生一个特殊的外键,它不像models.ForeignKey那样,必须指定一个Model来作为它指向的对象。Generic relations可以指向任何Model对象,有点像C语言中 void* 指针。
这样关于保存用户所产生的这个动作,比如用户写了一片日志,我们就可以使用Generic relations来指向某个Model实例比如Post,而那个Post实例才真正保存着关于用户动作的完整信息,即Post实例本身就是保存动作信息最好的地方。这样我们就可以通过存取Post实例里面的字段来描述用户的那个动作了,需要什么信息就往那里面去取。而且使用Generic relations的另外一个好处就是在删除了Post实例后,相应的新鲜事实例也会自动删除。
说了这么多,还是直接来看代码更实际些,看看到底怎么来实现吧。
假如有一个叫Post的Model,以及一个用来记录用户事件的Model定义如下:
# -*- coding: utf-8 -*-
from django.db import models
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
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)
def __unicode__(self):
return self.title
class Event(models.Model):
user = models.ForeignKey(User)
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
event = generic.GenericForeignKey('content_type', 'object_id')
created = models.DateTimeField(u'事件发生时间', auto_now_add = True)
def __unicode__(self):
return self.user.username + u'的事件'
Post这个Model很普通没什么好说的,Event这个Model有3个很特殊的字段,content_type是一个普通的外键指向一个叫ContentType的Model,而ContentType这个Model就很特殊了,文档描述是
Instances of ContentType represent and store information about the models installed in your project, and new instances of ContentType are automatically created whenever new models are installed.
就是说一个ContentType 实例 存储了 某个Model 的一些信息,通过这些信息就可以还原出那个Model。其实ContentType 的存储的信息也非常简单,其定义如下:
class ContentType(models.Model):
name = models.CharField(max_length=100)
app_label = models.CharField(max_length=100)
model = models.CharField(_('python model class name'), max_length=100)
objects = ContentTypeManager()
而通过app_label和model这2个字段,使用django.db.models.get_model这个方法就可以找出原来所对应的Model。而有了原来的Model的定义,再通过使用主键,就可以找到这个Model所对应的某条记录了,object_id这个字段正是用来存储这个主键的,
所以content_type和object_id这两个字段加起来就可以表达在一个project中所存在的所有Model的某个实例了。当然我们最终并不需要直接和这两个字段打交道,而是通过另外一个看起来很奇快的字段,generic.GenericForeignKey是一个特殊的外键,可以指向任何Model的实例,在这里就可以通过这个字段来指向类似Post这样保存着用户动作信息的Model实例。
使用Signal
定义完了Model之后就是来写一下signals部分的代码了,将原来的文件修改成如下这样:
# -*- coding: utf-8 -*-
from django.db import models
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
from django.db.models import signals
from django.dispatch import dispatcher
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 = generic.GenericRelation('Event')
def __unicode__(self):
return self.title
def description(self):
return u'%s 发表了日志《%s》' % (self.author, self.title)
class Admin:
pass
class Event(models.Model):
user = models.ForeignKey(User)
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
event = generic.GenericForeignKey('content_type', 'object_id')
created = models.DateTimeField(u'事件发生时间', auto_now_add = True)
def __unicode__(self):
return u"%s的事件: %s" % (self.user, self.description())
def description(self):
return self.event.description()
class Admin:
pass
def post_post_save(sender, instance, signal, *args, **kwargs):
post = instance
if post.created == post.updated:
event = Event(user=post.author,event = post)
event.save()
dispatcher.connect(post_post_save, signal=signals.post_save, sender=Post)
前面说到django在保存一个object的时候会发出一系列signals,在这里我们所监听的是signals.post_save这个signal,这个signal是在django保存完一个对象后发出的,利用dispatcher.connect这个函数来注册监听器,第一个参数是要执行的函数,第二个参数是要监听的signal,第三个参数是指定发送信号的Class,这里指定为Post这个Model,对其他Model所发出的signal并不会执行注册的函数。
而我们所定义的需要执行的函数可以将signal所发出的一些参数定义为需要执行的函数的参数,从而在函数里面进行使用,获取更多的信息,这里使用了instance这个参数,即刚刚保存完的Model对象实例。
在函数里通过比较日志发布时间和修改时间是否相等来判断是第一次发表还是修改后的保存,并且只为第一次发表这个动作创建一个相应的事件。创建事件的时候看到可以将post这个instance直接赋给generic.GenericForeignKey类型的字段,从而event实例就可以通过它来获取事件的真正信息了,在这里,如果有其他类型的Model实例,当然也可以赋值给generic.GenericForeignKey类型的字段了。
现在事件已经保存起来了,如何来得到事件的一个简短描述呢?可以给自己做一个假定,event这个字段所指向的Model都有一个叫做description的方法,那么我们只管调用这个方法就可以得到描述了。至于Post里面的那个description方法,可以根据需要,自己存取post实例的字段来获取信息,包括直接在描述字符串里面包含日志的url等。
最后有一点需要的注意的是,Post的Model定义里现在多了一个字段:
events = generic.GenericRelation('Event')
通过这个字段可以得到与某篇post相关联的所有事件,最重要的一点是如果没有这个字段,那么当删除一篇post的时候,与该post关联的事件是不会自动删除的。反之有这个字段就会进行自动的级联删除。
现在有了以上这些,如果你将这些Model定义和signals定义,添加到自己某个app,然后到admin页面添加一篇post试试,添加完后应该会自动多出了一条对应的event记录,修改一个post不会再添加event记录,而删除一篇post会自动删除相应的event记录。(现在django的newforms-admin分支已经合并了,可能关于本文的代码admin部分的定义已经无效,如何修改,可以参考此文: http://jinhao.iteye.com/blog/218112 )
参考:
- django拾遗之signal http://www.cnblogs.com/mindsbook/archive/2009/10/27/django_signal.html
- 使用Django的 signals 和 contenttypes 实现新鲜事功能 http://jinhao.iteye.com/blog/218346
- Django的ContentTypes和Generic Relations http://django-china.cn/topic/129/