書籍出處: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感受最實用。準備全書翻譯好後再抽時間把翻譯好的全部章節所有從新校對下!那麼你們下章再見!祈禱我年終中大獎!