书籍出处:https://www.packtpub.com/web-development/django-example
原做者:Antonio Meléjavascript
(译者注:无他,祝你们年会都中奖!)html
在上一章中,你在你的项目中实现了AJAX视图(views),经过使用jQuery并建立了一个JavaScript书签在你的平台中分享别的网站的内容。java
在本章中,你会学习如何建立一个粉丝系统以及建立一个用户活动流(activity stream)。你会发现Django信号(signals)的工做方式以及在你的项目中集成Redis快速 I/O 仓库用来存储 item 视图(views)。python
本章将会覆盖如下几点:linux
咱们将要在咱们的项目中建立一个粉丝系统。咱们的用户在平台中可以彼此关注而且跟踪其余用户的分享。这个关系在用户中的是多对多的关系,一个用户可以关注多个用户而且能被多个用户关注。web
在上一章中,你建立了多对对关系经过在其中一个有关联的模型(model)上添加了一个ManyToManyField而后让Django为这个关系建立了数据库表。这种方式支持大部分的场景,可是有时候你须要为这种关系建立一个中介模型(intermediate model)。建立一个中介模型(intermediate model)是很是有必要的当你想要为当前关系存储额外的信息,例如当前关系建立的时间点或者一个描述当前关系类型的字段。ajax
咱们会建立一个中介模型(intermediate model)用来在用户之间构建关系。有两个缘由能够解释为何咱们要用一个中介模型(intermediate model):redis
编辑你的account应用中的models.py文件添加以下代码:shell
from django.contrib.auth.models import User class Contact(models.Model): user_from = models.ForeignKey(User, related_name='rel_from_set') user_to = models.ForeignKey(User, related_name='rel_to_set') created = models.DateTimeField(auto_now_add=True, db_index=True) class Meta: ordering = ('-created',) def __str__(self): return '{} follows {}'.format(self.user_from, self.user_to)
这个Contact模型咱们将会给用户关系使用。它包含如下字段:数据库
auto_now_add=True
的DateTimeField字段用来存储关系建立时的时间在ForeignKey字段上会自动生成一个数据库索引。咱们使用db_index=True
来建立一个数据库索引给created字段。这会提高查询执行的效率当经过这个字段对查询集(QuerySets)进行排序的时候。
使用 ORM ,咱们能够建立一个关系给一个用户 user1 关注另外一个用户 user2,以下所示:
user1 = User.objects.get(id=1) user2 = User.objects.get(id=2) Contact.objects.create(user_from=user1, user_to=user2)
关系管理器 rel_form_set 和 rel_to_set 会返回一个查询集(QuerySets)给Contace模型(model)。为了
从User模型(model)中存取最终的关系侧,Contace模型(model)会指望User包含一个ManyToManyField,以下所示(译者注:如下代码是做者假设的,实际上User不会包含如下代码):
following = models.ManyToManyField('self', through=Contact, related_name='followers', symmetrical=False)
在这个例子中,咱们告诉Django去使用咱们定制的中介模型(intermediate model)来建立关系经过给ManyToManyField添加through=Contact
。这是一个从User模型到自己的多对对关系:咱们在ManyToMnyfIELD字段中引用 'self'
来建立一个关系给相同的模型(model)。
当你在多对多关系中须要额外的字段,建立一个定制的模型(model),一个关系侧就是一个ForeignKey。添加一个 ManyToManyField 在其中一个有关联的模型(models)中而后经过在through参数中包含该中介模型(intermediate model)指示Django去使用你的定制中介模型(intermediate model)。
若是User模型(model)是咱们应用的一部分,咱们能够添加以上的字段给模型(model)(译者注:因此说,上面的代码是做者假设存在)。但实际上,咱们没法直接修改User类,由于它是属于django.contrib.auth应用的。咱们将要作些轻微的改动,给User模型动态的添加这个字段。编辑account应用中的model.py文件,添加以下代码:
# Add following field to User dynamically User.add_to_class('following', models.ManyToManyField('self', through=Contact, related_name='followers', symmetrical=False))
在以上代码中,咱们使用Django模型(models)的add_to_class()
方法给User模型(model)添加monkey-patch(译者注:猴子补丁 Monkey patch 就是在运行时对已有的代码进行修改,而不须要修改原始代码)。你须要意识到,咱们不推荐使用add_to_class()
为模型(models)添加字段。咱们在这个场景中利用这种方法是由于如下的缘由:
user.followers.all()
以及user.following.all()
。咱们使用中介(intermediary) Contact 模型(model)能够避免复杂的查询例如使用到额外的数据库操做joins,若是在咱们的定制Profile模型(model)中定义过了关系。请记住,在大部分的场景中,在咱们以前建立的Profile模型(model)添加字段是更好的方法,能够替代在User模型(model)上打上monkey-patch。Django还容许你使用定制的用户模型(models)。若是你想要使用你的定制用户模型(model),能够访问 https://docs.djangoproject.com/en/1.8/topics/auth/customizing/#specifying-a-custom-user-model 得到更多信息。
你能看到上述代码中的关系包含了symmetrical=Flase
来定义一个非对称(non-symmetric)关系。这表示若是我关注了你,你不会自动的关注我。
当你使用了一个中介模型(intermediate model)给多对多关系,一些关系管理器的方法将不可用,例如:
add()
,create()
以及remove()
。你须要建立或删除中介模型(intermediate model)的实例来代替。
运行以下命令来生成account应用的初始迁移:
python manage.py makemigrations account
你会看到以下输出:
Migrations for 'account': 0002_contact.py: - Create model Contact
如今继续运行如下命令来同步应用到数据库中:
python manage.py migrate account
你会看到以下内容包含在输出中:
Applying account.0002_contact... OK
Contact模型(model)如今已经被同步进了数据库,咱们能够在用户之间建立关系。可是,咱们的网站尚未提供一个方法来浏览用户或查看详细的用户profile。让咱们为User模型构建列表和详情视图(views)。
打开account应用中的views.py文件添加以下代码:
from django.shortcuts import get_object_or_404 from django.contrib.auth.models import User @login_required def user_list(request): users = User.objects.filter(is_active=True) return render(request, 'account/user/list.html', {'section': 'people', 'users': users}) @login_required def user_detail(request, username): user = get_object_or_404(User, username=username, is_active=True) return render(request, 'account/user/detail.html', {'section': 'people', 'user': user})
以上是User对象的简单列表和详情视图(views)。user_list
视图(view)得到了全部的可用用户。Django User 模型(model)包含了一个标志(flag)is_active
来指示用户帐户是否可用。咱们经过is_active=True
来过滤查询只返回可用的用户。这个视图(vies)返回了全部结果,可是你能够改善它经过添加页码,这个方法咱们在image_list视图(view)中使用过。
user_detail视图(view)使用get_object_or_404()
快捷方法来返回全部可用的用户经过传入的用户名。当使用传入的用户名没法找到可用的用户这个视图(view)会返回一个HTTP 404响应。
编辑account应用的urls.py文件,为以上两个视图(views)添加URL模式,以下所示:
urlpatterns = [ # ... url(r'^users/$', views.user_list, name='user_list'), url(r'^users/(?P<username>[-\w]+)/$', views.user_detail, name='user_detail'), ]
咱们会使用 user_detail
URL模式来给用户生成规范的URL。你以前就在模型(model)中定义了一个get_absolute_url()
方法来为每一个对象返回规范的URL。另一种方式为一个模型(model)指定一个URL是为你的项目添加ABSOLUTE_URL_OVERRIDES设置。
编辑项目中的setting.py文件,添加以下代码:
ABSOLUTE_URL_OVERRIDES = { 'auth.user': lambda u: reverse_lazy('user_detail', args=[u.username]) }
Django会为全部出如今ABSOLUTE_URL_OVERRIDES设置中的模型(models)动态添加一个get_absolute_url()
方法。这个方法会给设置中指定的模型返回规范的URL。咱们给传入的用户返回user_detail URL。如今你能够在一个User实例上使用get_absolute_url()
来取回他自身的规范URL。打开Python shell输入命令python manage.py shell
运行如下代码来进行测试:
>>> from django.contrib.auth.models import User >>> user = User.objects.latest('id') >>> str(user.get_absolute_url()) '/account/users/ellington/'
返回的URL如同指望的同样。咱们须要为咱们刚才建立的视图(views)建立模板(templates)。在account应用下的*templates/account/目录下添加如下目录和文件:
/user/ detail.html list.html
编辑account/user/list.html模板(template)给它添加以下代码:
{% extends "base.html" %} {% load thumbnail %} {% block title %}People{% endblock %} {% block content %} <h1>People</h1> <div id="people-list"> {% for user in users %} <div class="user"> <a href="{{ user.get_absolute_url }}"> {% thumbnail user.profile.photo "180x180" crop="100%" as im %}  {% endthumbnail %} </a> <div class="info"> <a href="{{ user.get_absolute_url }}" class="title"> {{ user.get_full_name }} </a> </div> </div> {% endfor %} </div> {% endblock %}
这个模板(template)容许咱们在网站中排列全部可用的用户。咱们对给予的用户进行迭代而且使用`{% thumbnail %}模板(template)标签(tag)来生成profile图片缩微图。
打开项目中的base.html模板(template),在如下菜单项的href属性中包含user_listURL:
<li {% if section == "people" %}class="selected"{% endif %}> <a href="{% url "user_list" %}">People</a> </li>
经过命令python manage.py runserver
启动开发服务器而后在浏览器打开 http://127.0.0.1:8000/account/users/ 。你会看到以下所示的用户列:
(译者注:图灵,特斯拉,爱因斯坦,都是大牛啊)
编辑account应用下的account/user/detail.html模板,添加以下代码:
{% extends "base.html" %} {% load thumbnail %} {% block title %}{{ user.get_full_name }}{% endblock %} {% block content %} <h1>{{ user.get_full_name }}</h1> <div class="profile-info"> {% thumbnail user.profile.photo "180x180" crop="100%" as im %}  {% endthumbnail %} </div> {% with total_followers=user.followers.count %} <span class="count"> <span class="total">{{ total_followers }}</span> follower{{ total_followers|pluralize }} </span> <a href="#" data-id="{{ user.id }}" data-action="{% if request.user in user.followers.all %}un{% endif %}follow" class="followbutton"> {% if request.user not in user.followers.all %} Follow {% else %} Unfollow {% endif %} </a> <div id="image-list" class="imget-container"> {% include "images/image/list_ajax.html" with images = user.images_create.all %} </div> {% endwith %} {% endblock %}
在详情模板(template)中咱们展现用户profile而且咱们使用{% thumbnail %}
模板(template)标签(tag)来显示profile图片。咱们显示粉丝的总数以及一个连接能够 follow/unfollow 该用户。咱们会隐藏关注连接当用户在查看他们本身的profile,防止用户本身关注本身。咱们会执行一个AJAX请求来 follow/unfollow 一个指定用户。咱们给 <a>
HTML元素添加data-id和data-action属性包含用户ID以及当该连接被点击的时候会执行的初始操做,follow/unfollow ,这个操做依赖当前页面的展现的用户是否已经被正在浏览的用户所关注。咱们展现当前页面用户的图片书签经过list_ajax.html模板。
再次打开你的浏览器,点击一个拥有图片书签的用户连接,你会看到一个profile详情以下所示:
咱们将会建立一个简单的视图(view)使用AJAX来 follow/unfollow 用户。编辑account应用中的views.py文件添加以下代码:
from django.http import JsonResponse from django.views.decorators.http import require_POST from common.decorators import ajax_required from .models import Contact @ajax_required @require_POST @login_required def user_follow(request): user_id = request.POST.get('id') action = request.POST.get('action') if user_id and action: try: user = User.objects.get(id=user_id) if action == 'follow': Contact.objects.get_or_create( user_from=request.user, user_to=user) else: Contact.objects.filter(user_from=request.user, user_to=user).delete() return JsonResponse({'status':'ok'}) except User.DoesNotExist: return JsonResponse({'status':'ko'}) return JsonResponse({'status':'ko'})
user_follow视图(view)有点相似与咱们以前建立的image_like视图(view)。由于咱们使用了一个定制中介模型(intermediate model)给用户的多对多关系,因此ManyToManyField管理器默认的add()
和remove()
方法将不可用。咱们使用中介Contact模型(model)来建立或删除用户关系。
在account应用中的urls.py文件中导入你刚才建立的视图(view)而后为它添加URL模式:
url(r'^users/follow/$', views.user_follow, name='user_follow'),
请确保你放置的这个URL模式的位置在user_detailURL模式以前。不然,任何对 /users/follow/ 的请求都会被user_detail模式给正则匹配而后执行。请记住,每一次的HTTP请求Django都会对每一条存在的URL模式进行匹配直到第一条匹配成功才会中止继续匹配。
编辑account应用下的user/detail.html模板添加以下代码:
{% block domready %} $('a.follow').click(function(e){ e.preventDefault(); $.post('{% url "user_follow" %}', { id: $(this).data('id'), action: $(this).data('action') }, function(data){ if (data['status'] == 'ok') { var previous_action = $('a.follow').data('action'); // toggle data-action $('a.follow').data('action', previous_action == 'follow' ? 'unfollow' : 'follow'); // toggle link text $('a.follow').text( previous_action == 'follow' ? 'Unfollow' : 'Follow'); // update total followers var previous_followers = parseInt( $('span.count .total').text()); $('span.count .total').text(previous_action == 'follow' ? previous_followers + 1 : previous_followers - 1); } } }); }); {% endblock %}
这段JavaScript代码执行AJAX请求来关注或不关注一个指定用户而且触发 follow/unfollow 连接。咱们使用jQuery去执行AJAX请求的同时会设置 follow/unfollow 两种连接的data-aciton属性以及HTML<a>
元素的文本基于它上一次的值。当AJAX操做执行完成,咱们还会对显示在页面中的粉丝总数进行更新。打开一个存在的用户的详情页面,而后点击Follow连接尝试下咱们刚才构建的功能是否正常。
许多社交网站会给他们的用户显示一个活动流(activity stream),这样他们能够跟踪其余用户在平台中的操做。一个活动流(activity stream)是一个用户或一个用户组最近活动的列表。举个例子,Facebook的News Feed就是一个活动流(activity stream)。用户X给Y图片打上了书签或者用户X关注了用户Y也是例子操做。咱们将会构建一个活动流(activity stream)应用这样每一个用户都能看到他关注的用户最近进行的交互。为了作到上述功能,咱们须要一个模型(modes)来保存用户在网站上的操做执行,还须要一个简单的方法来添加操做给feed。
运行如下命令在你的项目中建立一个新的应用命名为actions:
django-admin startapp actions
在你的项目中的settings.py文件中的INSTALLED_APPS设置中添加'actions',这样可让Django知道这个新的应用是可用状态:
INSTALLED_APPS = ( # ... 'actions', )
编辑actions应用下的models.py文件添加以下代码:
from django.db import models from django.contrib.auth.models import User class Action(models.Model): user = models.ForeignKey(User, related_name='actions', db_index=True) verb = models.CharField(max_length=255) created = models.DateTimeField(auto_now_add=True, db_index=True) class Meta: ordering = ('-created',)
这个Action模型(model)将会用来记录用户的活动。模型(model)中的字段解释以下:
auto_now_add=True
来动态设置它为当前的时间当这个对象第一次被保存在数据库中。经过这个基础模型(model),咱们只可以存储操做例如用户X作了哪些事情。咱们须要一个额外的ForeignKey字段为了保存操做会涉及到的一个target(目标)对象,例如用户X给图片Y打上了暑期那或者用户X如今关注了用户Y。就像你以前知道的,一个普通的ForeignKey只能指向一个其余的模型(model)。可是,咱们须要一个方法,可让操做的target(目标)对象是任何一个已经存在的模型(model)的实例。这个场景就由Django内容类型框架来上演。
Django包含了一个内容类型框架位于django.contrib.contenttypes。这个应用能够跟踪你的项目中全部的模型(models)以及提供一个通用接口来与你的模型(models)进行交互。
当你使用startproject命令建立一个新的项目的时候这个contenttypes应用就被默认包含在INSTALLED_APPS设置中。它被其余的contrib包使用,例如认证(authentication)框架以及admin应用。
contenttypes应用包含一个ContentType模型(model)。这个模型(model)的实例表明了你的应用中真实存在的模型(models),而且新的ContentTYpe实例会动态的建立当新的模型(models)安装在你的项目中。ContentType模型(model)有如下字段:
让咱们看一下咱们如何实例化ContentType对象。打开Python终端使用python manage.py shell
命令。你能够获取一个指定模型(model)对应的ContentType对象经过执行一个带有app_label和model属性的查询,例如:
>>> from django.contrib.contenttypes.models import ContentType >>> image_type = ContentType.objects.get(app_label='images',model='image') >>> image_type <ContentType: image>
你还能反过来获取到模型(model)类从一个ContentType对象中经过调用它的model_class()
方法:
>>> from images.models import Image >>> ContentType.objects.get_for_model(Image) <ContentType: image>
以上就是内容类型的一些例子。Django提供了更多的方法来使用他们进行工做。你能够访问 https://docs.djangoproject.com/en/1.8/ref/contrib/contenttypes/ 找到关于内容类型框架的官方文档。
在通用关系中ContentType对象扮演指向模型(model)的角色被关联所使用。你须要3个字段在模型(model)中组织一个通用关系:
编辑actions应用的models.py文件,添加以下代码:
from django.db import models from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey class Action(models.Model): user = models.ForeignKey(User, related_name='actions', db_index=True) verb = models.CharField(max_length=255) target_ct = models.ForeignKey(ContentType, blank=True, null=True, related_name='target_obj') target_id = models.PositiveIntegerField(null=True, blank=True, db_index=True) target = GenericForeignKey('target_ct', 'target_id') created = models.DateTimeField(auto_now_add=True, db_index=True) class Meta: ordering = ('-created',)
咱们给Action模型添加了如下字段:
Django没有建立任何字段在数据库中给GenericForeignKey字段。只有target_ct和target_id两个字段被映射到数据库字段。两个字段都有blank=True
和null=True
属性因此一个target(目标)对象不是必须的当保存Action对象的时候。
你可让你的应用更加灵活经过使用通用关系替代外键当它对拥有一个通用关系有意义。
运行如下命令来建立初始迁移为这个应用:
python manage.py makemigrations actions
你会看到以下输出:
Migrations for 'actions': 0001_initial.py: - Create model Action
接着,运行下一条命令来同步应用到数据库中:
python manage.py migrate
这条命令的输出代表新的迁移已经被应用:
Applying actions.0001_initial... OK
让咱们在管理站点中添加Action模型(model)。编辑actions应用的admin.py文件,添加以下代码:
from django.contrib import admin from .models import Action class ActionAdmin(admin.ModelAdmin): list_display = ('user', 'verb', 'target', 'created') list_filter = ('created',) search_fields = ('verb',) admin.site.register(Action, ActionAdmin)
你已经将Action模型(model)注册到了管理站点中。运行命令python manage.py runserver
来初始化开发服务器而后在浏览器中打开 http://127.0.0.1:8000/admin/actions/action/add/ 。你会看到以下页面能够建立一个新的Action对象:
如你所见,只有target_ct和target_id两个字段是映射为真实的数据库字段显示,而且GenericForeignKey字段不在这儿出现。target_ct容许你选择任何一个在你的Django项目中注册的模型(models)。你能够限制内容类型从一个限制的模型(models)集合中选择经过在target-ct字段中使用limit_choices_to属性:limit_choices_to属性容许你限制ForeignKey字段的内容经过给予一个特定值的集合。
在actions应用目录下建立一个新的文件命名为utils.py。咱们会定义一个快捷函数,该函数容许咱们使用一种简单的方式建立新的Action对象。编辑这个新的文件添加以下代码给它:
from django.contrib.contenttypes.models import ContentType from .models import Action def create_action(user, verb, target=None): action = Action(user=user, verb=verb, target=target) action.save()
create_action()
函数容许咱们建立actions,该actions能够包含一个target对象或不包含。咱们可使用这个函数在咱们代码的任何地方添加新的actions给活动流(activity stream)。
有时候你的用户可能屡次执行同个动做。他们可能在短期内屡次点击 like/unlike 按钮或者屡次执行一样的动做。这会致使你中止存储和显示重复的动做。为了不这种状况咱们须要改善create_action()
函数来避免大部分的重复动做。
编辑actions应用中的utils.py文件使它看上去以下所示:
import datetime from django.utils import timezone from django.contrib.contenttypes.models import ContentType from .models import Action def create_action(user, verb, target=None): # check for any similar action made in the last minute now = timezone.now() last_minute = now - datetime.timedelta(seconds=60) similar_actions = Action.objects.filter(user_id=user.id, verb= verb, timestamp__gte=last_minute) if target: target_ct = ContentType.objects.get_for_model(target) similar_actions = similar_actions.filter( target_ct=target_ct, target_id=target.id) if not similar_actions: # no existing actions found action = Action(user=user, verb=verb, target=target) action.save() return True return False
咱们经过修改create_action()
函数来避免保存重复的动做而且返回一个布尔值来告诉该动做是否保存。下面来解释咱们是如何避免重复动做的:
timezone.now()
方法来获取当前时间。这个方法同datetime.datetime.now()相同,可是返回的是一个*timezone-aware*对象。Django提供一个设置叫作*USE_TZ*用来启用或关闭时区的支持。经过使用*startproject*命令建立的默认*settings.py*包含
USE_TZ=True`。是时候添加一些动做给咱们的视图(views)来给个人用户构建活动流(activity stream)了。咱们将要存储一个动做为如下的每个实例:
编辑images应用下的views.py文件添加如下导入:
from actions.utils import create_action
在image_create视图(view)中,在保存图片以后添加create-action()
,以下所示:
new_item.save() create_action(request.user, 'bookmarked image', new_item)
在image_like视图(view)中,在添加用户给users_like关系以后添加create_action()
,以下所示:
image.users_like.add(request.user) create_action(request.user, 'likes', image)
如今编辑account应用中的view.py文件添加如下导入:
from actions.utils import create_action
在register视图(view)中,在建立Profile对象以后添加create-action()
,以下所示:
new_user.save() profile = Profile.objects.create(user=new_user) create_action(new_user, 'has created an account')
在user_follow视图(view)中添加create_action()
,以下所示:
Contact.objects.get_or_create(user_from=request.user,user_to=user) create_action(request.user, 'is following', user)
就像你所看到的,感谢咱们的Action模型(model)和咱们的帮助函数,如今保存新的动做给活动流(activity stream)是很是简单的。
最后,咱们须要一种方法来给每一个用户显示活动流(activity stream)。咱们将会在用户的dashboard中包含活动流(activity stream)。编辑account应用的views.py文件。导入Action模型而后修改dashboard视图(view)以下所示:
from actions.models import Action @login_required def dashboard(request): # Display all actions by default actions = Action.objects.exclude(user=request.user) following_ids = request.user.following.values_list('id',flat=True) if following_ids: # If user is following others, retrieve only their actions actions = actions.filter(user_id__in=following_ids) actions = actions[:10] return render(request, 'account/dashboard.html', {'section': 'dashboard', 'actions': actions})
在这个视图(view),咱们从数据库取回全部的动做(actions),不包含当前用户执行的动做。若是当前用户尚未关注过任何人,咱们展现在平台中的其余用户的最新动做执行。这是一个默认的行为当当前用户尚未关注过任何其余的用户。若是当前用户已经关注了其余用户,咱们就限制查询只显示当前用户关注的用户的动做执行。最后,咱们限制结果只返回最前面的10个动做。咱们在这儿并不使用order_by()
,由于咱们依赖以前已经在Action模型(model)的Meta的排序选项。最新的动做会首先返回,由于咱们在Action模型(model)中设置过ordering = ('-created',)
。
每次你取回一个Aciton对象,你均可能存取它的有关联的User对象,
而且可能这个用户也关联它的Profile对象。Django ORM提供了一个简单的方式一次性取回有关联的对象,避免对数据库进行额外的查询。
Django提供了一个叫作select_related()
的查询集(QuerySets)方法容许你取回关系为一对多的关联对象。该方法将会转化成一个单独的,更加复杂的查询集(QuerySets),可是你能够避免额外的查询当存取这些关联对象。select_relate方法是给ForeignKey和OneToOne字段使用的。它经过执行一个 SQL JOIN而且包含关联对象的字段在SELECT 声明中。
为了利用select_related()
,编辑以前代码中的如下行(译者注:请注意双下划线):
actions = actions.filter(user_id__in=following_ids)
添加select_related在你将要使用的字段上:
actions = actions.filter(user_id__in=following_ids)\ .select_related('user', 'user__profile')
咱们使用user__profile
(译者注:请注意是双下划线)来链接profile表在一个单独的SQL查询中。若是你调用select_related()
而不传入任何参数,它会取回全部ForeignKey关系的对象。给select_related()
限制的关系将会在随后一直访问。
当心的使用
select_related()
将会极大的提升执行时间
如你所见,select_related()
将会帮助你提升取回一对多关系的关联对象的执行效率。可是,select_related()
没法给多对多或者多对一关系(ManyToMany或者倒转ForeignKey字段)工做。Django提供了一个不一样的查询集(QuerySets)方法叫作prefetch_realted,该方法在select_related()
方法支持的关系上增长了多对多和多对一的关系。prefetch_related()
方法为每一种关系执行单独的查找而后对各个结果进行链接经过使用Python。这个方法还支持GeneriRelation和GenericForeignKey的预先读取。
完成你的查询经过为它添加prefetch_related()
给目标GenericForeignKey字段,以下所示:
actions = actions.filter(user_id__in=following_ids)\ .select_related('user', 'user__profile')\ .prefetch_related('target')
这个查询如今已经被充分利用用来取回包含关联对象的用户动做(actions)。
咱们要建立一个模板(template)用来显示一个独特的Action对象。在actions应用中建立一个新的目录命名为templates。添加以下文件结构:
actions/ action/ detail.html
编辑actions/action/detail.html模板(template)文件添加以下代码:
明天添加
这个模板用来显示一个Action对象。首先,咱们使用{% with %}
模板标签(template tag)来获取用户操做的动做(action)和他们的profile。而后,咱们显示目标对象的图片若是Action对象有一个关联的目标对象。最后,若是有执行过的动做(action),包括动做和目标对象,咱们就显示连接给用户。
如今,编辑account/dashboard.html模板(template)添加以下代码到content区块下方:
<h2>What's happening</h2> <div id="action-list"> {% for action in actions %} {% include "actions/action/detail.html" %} {% endfor %} </div>
在浏览器中打开 http://127.0.0.1:8000/account/ 。登陆一个存在的用户而且该用户执行过一些操做已经被存储在数据库中。而后,登陆其余用户,关注以前登陆的用户,在dashboard页面能够看到生成的动做流。以下所示:
咱们刚刚建立了一个完整的活动流(activity stream)给咱们的用户而且咱们还能很是容易的添加新的用户动做给它。你还能够添加无限的滚动功能给活动流(activity stream)经过集成AJAX分页处理,和咱们以前在image_list视图(view)使用过的同样。
有一些场景,你想要使你的数据非规范化。非规划化使指在必定的程度上制造一些数据冗余用来优化读取的性能。你必须十分当心的使用非规划化而且只有在你真的很是须要它的时候才能使用。你会发现非规划化的最大问题就是保持你的非规范化数据更新是很是困难的。
咱们将会看到一个例子关于如何改善(improve)咱们的查询经过使用非规范化计数。缺点就是咱们不得不保持冗余数据的更新。咱们将要从咱们的Image模型(model)中使数据非规范化而后使用Django信号来保持数据的更新。
Django自带一个信号调度程序容许receiver函数在某个动做出现的时候去获取通知。信号很是有用,当你须要你的代码去执行某些事件的时候同时正在发生其余事件。你还可以建立你本身的信号这样一来其余人能够在某个事件发生的时候得到通知。
Django模型(models)提供了几个信号,它们位于django.db.models.signales。举几个例子:
save()
方法前发送信号,后者反之。delete()
方法以前发送信号,后者反之。以上只是Django提供的一小部分信号。你能够经过访问 https://docs.djangoproject.com/en/1.8/ref/signals/ 得到更多信号资料。
打个比方,你想要获取热门图片。你可使用Django的聚合函数来获取图片,经过图片获取的用户喜欢数量来进行排序。要记住你已经使用过Django聚合函数在第三章 扩展你的blog应用。如下代码将会获取图片并进行排序经过它们被用户喜欢的数量:
from django.db.models import Count from images.models import Image images_by_popularity = Image.objects.annotate( total_likes=Count('users_like')).order_by('-total_likes')
可是,经过统计图片的总喜欢数量进行排序比直接使用一个已经存储总统计数的字段进行排序要消耗更多的性能。你能够添加一个字段给Image模型(model)用来非规范化喜欢的数量用来提高涉及该字段的查询的性能。那么,问题来了,咱们该如何保持这个字段是最新更新过的。
编辑images应用下的models.py文件,给Image模型(model)添加如下字段:
total_likes = models.PositiveIntegerField(db_index=True, default=0)
total_likes字段容许咱们给每张图片存储被用户喜欢的总数。非规范化数据很是有用当你想要使用他们来过滤或排序查询集(QuerySets)。
在你使用非规范化字段以前你必须考虑下其余几种提升性能的方法。考虑下数据库索引,最佳化查询以及缓存在开始规范化你的数据以前。
运行如下命令将新添加的字段迁移到数据库中:
python manage.py makemigrations images
你会看到以下输出:
Migrations for 'images': 0002_image_total_likes.py: - Add field total_likes to image
接着继续运行如下命令来应用迁移:
python manage.py migrate images
输出中会包含如下内容:
Applying images.0002_image_total_likes... OK
咱们要给m2m_changed信号附加一个receiver函数。在images应用目录下建立一个新的文件命名为signals.py。给该文件添加以下代码:
from django.db.models.signals import m2m_changed from django.dispatch import receiver from .models import Image @receiver(m2m_changed, sender=Image.users_like.through) def users_like_changed(sender, instance, **kwargs): instance.total_likes = instance.users_like.count() instance.save()
首先,咱们使用receiver()
装饰器将users_like_changed
函数注册成一个receiver函数,而后咱们将该函数附加给m2m_changed信号。咱们将这个函数与Image.users_like.through链接,这样这个函数只有当m2m_changed信号被Image.users_like.through执行的时候才被调用。还有一个能够替代的方式来注册一个receiver函数,由使用Signal对象的connect()
方法组成。
Django信号是同步阻塞的。不要使用异步任务致使信号混乱。可是,你能够联合二者来执行异步任务当你的代码只接受一个信号的通知。
你必须链接你的receiver函数给一个信号,只有这样它才会被调用当链接的信号发送的时候。有一个推荐的方法用来注册你的信号是在你的应用配置类中导入它们到ready()
方法中。Django提供一个应用注册容许你对你的应用进行配置和内省。
django容许你指定配置类给你的应用们。为了提供一个自定义的配置给你的应用,建立一个继承django.apps的Appconfig类的自定义类。这个应用配置类容许你为应用存储元数据和配置而且提供
内省。
你能够经过访问 https://docs. djangoproject.com/en/1.8/ref/applications/ 获取更多关于应用配置的信息。
为了注册你的信号receiver函数,当你使用receiver()
装饰器的时候,你只须要导入信号模块,这些信号模块被包含在你的应用的AppConfig类中的ready()
方法中。这个方法在应用注册被完整填充的时候就调用。其余给你应用的初始化均可以被包含在这个方法中。
在images应用目录下建立一个新的文件命名为apps.py。为该文件添加以下代码:
from django.apps import AppConfig class ImagesConfig(AppConfig): name = 'images' verbose_name = 'Image bookmarks' def ready(self): # import signal handlers import images.signals
name属性定义该应用完整的Python路径。verbose_name属性设置了这个应用可读的名字。它会在管理站点中显示。ready()
方法就是咱们为这个应用导入信号的地方。
如今咱们须要告诉Django咱们的应用配置位于哪里。编辑位于images应用目录下的init.py文件添加以下内容:
default_app_config = 'images.apps.ImagesConfig'
打开你的浏览器浏览一个图片的详细页面而后点击like按钮。再进入管理页面看下该图片的total_like属性。你会看到total_likes属性已经更新了最新的like数以下所示:
如今,你可使用totla_likes属性来进行热门图片的排序或者在任何地方显示这个值,从而避免了复杂的查询操做。如下获取图片的查询经过图片的喜欢数量进行排序:
images_by_popularity = Image.objects.annotate( likes=Count('users_like')).order_by('-likes')
如今咱们能够用新的查询来代替上面的查询:
images_by_popularity = Image.objects.order_by('-total_likes')
以上查询的返回结果只须要不多的SQL查询性能。以上就是一个例子关于如何使用Django信号。
当心使用信号,由于它们会给理解控制流制造困难。在不少场景下你能够避免使用信号若是你知道哪一个接收器须要被通知。
Redis是一个高级的key-value(键值)数据库容许你保存不一样类型的数据而且在I/O(输入/输出)操做上很是很是的快速。Redis能够在内存中存储任何东西,可是这些数据可以持续经过偶尔存储数据集到磁盘中或者添加每一条命令到日志中。Redis是很是出彩的经过与其余的键值存储对比:它提供了一个强大的设置命令,而且支持多种数据结构,例如string,hashes,lists,sets,ordered sets,甚至bitmaps和HyperLogLogs。
SQL最适合用于模式定义的持续数据存储,而Redis提供了许多优点当须要处理快速变化的数据,易失性存储,或者须要一个快速缓存的时候。让咱们看下Redis是如何被使用的,当构建新的功能到咱们的项目中。
从 http://redis.io/download 下载最新的Redis版本。解压tar.gz文件,进入redis目录而后编译Redis经过使用如下make命令:
cd redis-3.0.4(版本根据本身下载的修改) make (这里是假设你使用的是linux或者mac系统才用make,windows如何操做请看下官方文档)
在Redis安装完成后容许如下shell命令来初始化Redis服务:
src/redis-server
你会看到输出的结尾以下所示:
# Server started, Redis version 3.0.4 * DB loaded from disk: 0.001 seconds * The server is now ready to accept connections on port 6379
默认的,Redis运行会占用6379端口,可是你也能够指定一个自定义的端口经过使用--port
标志,例如:redis-server --port 6655
。当你的服务启动完毕,你能够在其余的终端中打开Redis客户端经过使用以下命令:
src/redis-cli
你会看到Redis客户端shell以下所示:
127.0.0.1:6379>
Redis客户端容许你在当前shell中当即执行Rdis命令。来咱们来尝试一些命令。键入SET命令在Redis客户端中存储一个值到一个键中:
127.0.0.1:6379> SET name "Peter" ok
以上的命令建立了一个带有字符串“Peter”值的name键到Redis数据库中。OK输出代表该键已经被成功保存。而后,使用GET命令获取以前的值,以下所示:
127.0.0.1:6379> GET name "Peter"
你还能够检查一个键是否存在经过使用EXISTS命令。若是检查的键存在会返回1,反之返回0:
127.0.0.1:6379> EXISTS name (integer) 1
你能够给一个键设置到期时间经过使用EXPIRE命令,该命令容许你设置该键能在几秒内存在。另外一个选项使用EXPIREAT命令来指望一个Unix时间戳。键的到期消失是很是有用的当将Redis当作缓存使用或者存储易失性的数据:
127.0.0.1:6379> GET name "Peter" 127.0.0.1:6379> EXPIRE name 2 (integer) 1 Wait for 2 seconds and try to get the same key again: 127.0.0.1:6379> GET name (nil)
(nil)响应是一个空的响应说明没有找到键。你还能够经过使用DEL命令删除任意键,以下所示:
127.0.0.1:6379> SET total 1 OK 127.0.0.1:6379> DEL total (integer) 1 127.0.0.1:6379> GET total (nil)
以上只是一些键选项的基本命令。Redis包含了庞大的命令设置给一些数据类型,例如strings,hashes,sets,ordered sets等等。你能够经过访问 http://redis.io/commands 看到全部Reids命令以及经过访问 http://redis.io/topics/data-types 看到全部Redis支持的数据类型。
咱们须要绑定Python和Redis。经过pip渠道安装redis-py命令以下:
pip install redis==2.10.3(译者注:版本可能有更新,若是须要最新版本,能够不带上'==2.10.3'后缀)
你能够访问 http://redis-py.readthedocs.org/ 获得redis-py文档。
redis-py提供两个类用来与Redis交互:StrictRedis和Redis。二者提供了相同的功能。StrictRedis类尝试遵照官方的Redis命令语法。Redis类型继承Strictredis重写了部分方法来提供向后的兼容性。咱们将会使用StrictRedis类,由于它遵照Redis命令语法。打开Python shell执行如下命令:
>>> import redis >>> r = redis.StrictRedis(host='localhost', port=6379, db=0)
上面的代码建立了一个与Redis数据库的链接。在Redis中,数据库经过一个整形索引替代数据库名字来辨识。默认的,一个客户端被链接到数据库 0 。Reids数据库可用的数字设置到16,可是你能够在redis.conf文件中修改这个值。
如今使用Python shell设置一个键:
>>> r.set('foo', 'bar') True
以上命令返回Ture代表这个键已经建立成功。如今你可使用get()
命令取回该键:
>>> r.get('foo') 'bar'
如你所见,StrictRedis方法遵照Redis命令语法。
让咱们集成Rdies到咱们的项目中。编辑bookmarks项目的settings.py文件添加以下设置:
REDIS_HOST = 'localhost' REDIS_PORT = 6379 REDIS_DB = 0
以上设置了Redis服务器和咱们将要在项目中使用到的数据库。
让咱们存储一张图片被查看的总次数。若是咱们经过Django ORM来完成这个操做,它会在每次该图片显示的时候执行一次SQL UPDATE声明。使用Redis,咱们只须要对一个计数器进行增量存储在内存中,从而带来更好的性能。
编辑images应用下的views.py文件,添加以下代码:
import redis from django.conf import settings # connect to redis r = redis.StrictRedis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_DB)
在这儿咱们创建了Redis的链接为了能在咱们的视图(views)中使用它。编辑images_detail视图(view)使它看上去以下所示:
def image_detail(request, id, slug): image = get_object_or_404(Image, id=id, slug=slug) # increment total image views by 1 total_views = r.incr('image:{}:views'.format(image.id)) return render(request, 'images/image/detail.html', {'section': 'images', 'image': image, 'total_views': total_views})
在这个视图(view)中,咱们使用INCR命令,它会从1开始增量一个键的值,在执行这个操做以前若是键不存在,它会将值设定为0.incr()
方法在执行操做后会返回键的值,而后咱们能够存储该值到total_views变量中。咱们构建Rddis键使用一个符号,好比 object-type🆔field (for example image:33:id) 。
对Redis的键进行命名有一个惯例是使用冒号进行分割来建立键的命名空间。作到这点,键的名字会特别冗长,有关联的键会分享部分相同的模式在它们的名字中。
编辑image/detail.html模板(template)在已有的<span class="count">
元素以后添加以下代码:
<span class="count"> <span class="total">{{ total_views }}</span> view{{ total_views|pluralize }} </span>
如今在浏览器中打开一张图片的详细页面而后屡次加载该页面。你会看到每次该视图(view)被执行的时候,总的观看次数会增长 1 。以下所示:
你已经成功的集成Redis到你的项目中来存储项统计。
让咱们使用Reids构建更多的功能。咱们要在咱们的平台中建立一个最多浏览次数的图片排行。为了构建这个排行咱们将要使用Redis分类集合。一个分类集合是一个非重复的字符串采集,其中每一个成员和一个分数关联。其中的项根据它们的分数进行排序。
编辑images引用下的views.py文件,使image_detail视图(view)看上去以下所示:
def image_detail(request, id, slug): image = get_object_or_404(Image, id=id, slug=slug) # increment total image views by 1 total_views = r.incr('image:{}:views'.format(image.id)) # increment image ranking by 1 r.zincrby('image_ranking', image.id, 1) return render(request, 'images/image/detail.html', {'section': 'images', 'image': image, 'total_views': total_views})
咱们使用zincrby()
命令存储图片视图(views)到一个分类集合中经过键image:ranking
。咱们存储图片id,和一个分数1,它们将会被加到分类集合中这个元素的总分上。这将容许咱们在全局上持续跟踪全部的图片视图(views),而且有一个分类集合,该分类集合经过图片的浏览次数进行排序。
如今建立一个新的视图(view)用来展现最多浏览次数图片的排行。在views.py文件中添加以下代码:
@login_required def image_ranking(request): # get image ranking dictionary image_ranking = r.zrange('image_ranking', 0, -1, desc=True)[:10] image_ranking_ids = [int(id) for id in image_ranking] # get most viewed images most_viewed = list(Image.objects.filter( id__in=image_ranking_ids)) most_viewed.sort(key=lambda x: image_ranking_ids.index(x.id)) return render(request, 'images/image/ranking.html', {'section': 'images', 'most_viewed': most_viewed})
以上就是image_ranking视图。咱们使用zrange()
命令得到分类集合中的元素。这个命令指望一个自定义的范围,最低分和最高分。经过将 0 定为最低分, -1 为最高分,咱们告诉Redis返回分类集合中的全部元素。最终,咱们使用[:10]
对结果进行切片获取最前面十个最高分的元素。咱们构建一个返回的图片IDs的列,而后咱们将该列存储在image_ranking_ids变量中,这是一个整数列。咱们经过这些IDs取回对应的Image对象,并将它们强制转化为列经过使用list()
函数。强制转化查询集(QuerySets)的执行是很是重要的,由于接下来咱们要在该列上使用列的sort()
方法(就是由于这点因此咱们须要的是一个对象列而不是一个查询集(QuerySets))。咱们排序这些Image对象经过它们在图片排行中的索引。如今咱们能够在咱们的模板(template)中使用most_viewed列来显示10个最多浏览次数的图片。
建立一个新的image/ranking.html模板(template)文件,添加以下代码:
{% extends "base.html" %} {% block title %}Images ranking{% endblock %} {% block content %} <h1>Images ranking</h1> <ol> {% for image in most_viewed %} <li> <a href="{{ image.get_absolute_url }}"> {{ image.title }} </a> </li> {% endfor %} </ol> {% endblock %}
这个模板(template)很是简单明了,咱们只是对包含在most_viewed中的Image对象进行迭代。
最后为新的视图(view)建立一个URL模式。编辑images应用下的urls.py文件,添加以下内容:
url(r'^ranking/$', views.image_ranking, name='create'),
在浏览器中打开 http://127.0.0.1:8000/images/ranking/ 。你会看到以下图片排行:
Redis并不能替代你的SQL数据库,可是它是一个内存中的快速存储,更适合某些特定任务。将它添加到你的栈中使用当你真的感受它很须要。如下是一些适合Redis的场景:
incr()
和`incrby()。lpush()
和rpush()
。移除和返回开头和结尾的元素经过使用lpop()
以及rpop()
。你能够削减列的长度经过使用ltrim()
来维持它的长度。expire()
和expireat()
容许你将Redis当成缓存使用。你还能够找到第三方的Reids缓存后台给Django使用。在本章中,你构建了一个粉丝系统和一个用户活动流(activity stream)。你学习了Django信号是如何进行工做而且在你的项目中集成了Redis。
在下一章中,你会学习到如何构建一个在线商店。你会建立一个产品目录而且经过会话(sessions)建立一个购物车。你还会学习如何经过Celery执行异步任务。
这一章好长啊!最后部分的Redis感受最实用。准备全书翻译好后再抽时间把翻译好的全部章节所有从新校对下!那么你们下章再见!祈祷我年终中大奖!