全文鏈接
第一章 創(chuàng)建一個(gè)blog應(yīng)用
第二章 使用高級(jí)特性來增強(qiáng)你的blog
第三章 擴(kuò)展你的blog應(yīng)用
第四章上 創(chuàng)建一個(gè)社交網(wǎng)站
第四章下 創(chuàng)建一個(gè)社交網(wǎng)站
第五章 在你的網(wǎng)站中分享內(nèi)容
第六章 跟蹤用戶動(dòng)作
第七章 建立一個(gè)在線商店
第八章 管理付款和訂單
第九章上 擴(kuò)展你的商店
第九章下 擴(kuò)展你的商店
第十章上 創(chuàng)建一個(gè)在線學(xué)習(xí)平臺(tái)
第十章下 創(chuàng)建一個(gè)在線學(xué)習(xí)平臺(tái)
第十一章 緩存內(nèi)容
第十二章 構(gòu)建一個(gè)API
書籍出處:https://www.packtpub.com/web-development/django-example
原作者:Antonio Melé
(譯者注:無他,祝大家年會(huì)都中獎(jiǎng)!)
第六章
跟蹤用戶動(dòng)作
在上一章中迂尝,你在你的項(xiàng)目中實(shí)現(xiàn)了AJAX視圖(views),通過使用jQuery并創(chuàng)建了一個(gè)JavaScript書簽在你的平臺(tái)中分享別的網(wǎng)站的內(nèi)容剪芥。
在本章中垄开,你會(huì)學(xué)習(xí)如何創(chuàng)建一個(gè)粉絲系統(tǒng)以及創(chuàng)建一個(gè)用戶活動(dòng)流(activity stream)。你會(huì)學(xué)習(xí)到Django信號(hào)(signals)的工作方式以及在你的項(xiàng)目中集成Redis快速 I/O 倉庫用來存儲(chǔ)視圖(views)項(xiàng)税肪。
本章將會(huì)覆蓋以下幾點(diǎn):
- 通過一個(gè)中介模型(intermediate model)創(chuàng)建多對(duì)對(duì)的關(guān)系
- 創(chuàng)建 AJAX 視圖(views)
- 創(chuàng)建一個(gè)活動(dòng)流(activity stream)應(yīng)用
- 給模型(modes)添加通用關(guān)系
- 取回對(duì)象的最優(yōu)查詢集(QuerySets)
- 使用信號(hào)(signals)給非規(guī)范化的計(jì)數(shù)
- 存儲(chǔ)視圖(views)項(xiàng)到 Redis 中
創(chuàng)建一個(gè)粉絲系統(tǒng)
我們將要在我們的項(xiàng)目中創(chuàng)建一個(gè)粉絲系統(tǒng)溉躲。我們的用戶在平臺(tái)中能夠彼此關(guān)注并且跟蹤其他用戶的分享。這個(gè)關(guān)系在用戶中的是多對(duì)多的關(guān)系益兄,一個(gè)用戶能夠關(guān)注多個(gè)用戶并且能被多個(gè)用戶關(guān)注锻梳。
通過一個(gè)中介模型(intermediate model)(intermediary model)創(chuàng)建多對(duì)對(duì)的關(guān)系
在上一章中,你創(chuàng)建了多對(duì)對(duì)關(guān)系通過在其中一個(gè)有關(guān)聯(lián)的模型(model)上添加了一個(gè)ManyToManyField然后讓Django為這個(gè)關(guān)系創(chuàng)建了數(shù)據(jù)庫表净捅。這種方式支持大部分的場景疑枯,但是有時(shí)候你需要為這種關(guān)系創(chuàng)建一個(gè)中介模型(intermediate model)。創(chuàng)建一個(gè)中介模型(intermediate model)是非常有必要的當(dāng)你想要為當(dāng)前關(guān)系存儲(chǔ)額外的信息灸叼,例如當(dāng)前關(guān)系創(chuàng)建的時(shí)間點(diǎn)或者一個(gè)描述當(dāng)前關(guān)系類型的字段神汹。
我們會(huì)創(chuàng)建一個(gè)中介模型(intermediate model)用來在用戶之間構(gòu)建關(guān)系庆捺。有兩個(gè)原因可以解釋為什么我們要用一個(gè)中介模型(intermediate model):
- 我們使用Django提供的user模型(model)并且我們想要避免修改它古今。
- 我們想要存儲(chǔ)關(guān)系建立的時(shí)間
編輯你的account應(yīng)用中的models.py文件添加如下代碼:
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)
這個(gè)Contact模型我們將會(huì)給用戶關(guān)系使用。它包含以下字段:
- user_form:一個(gè)ForeignKey指向創(chuàng)建關(guān)系的用戶
- user_to:一個(gè)ForeignKey指向被關(guān)注的用戶
- created:一個(gè)
auto_now_add=True
的DateTimeField字段用來存儲(chǔ)關(guān)系創(chuàng)建時(shí)的時(shí)間
在ForeignKey字段上會(huì)自動(dòng)生成一個(gè)數(shù)據(jù)庫索引滔以。我們使用db_index=True
來創(chuàng)建一個(gè)數(shù)據(jù)庫索引給created字段捉腥。這會(huì)提升查詢執(zhí)行的效率當(dāng)通過這個(gè)字段對(duì)查詢集(QuerySets)進(jìn)行排序的時(shí)候。
使用 ORM 你画,我們可以創(chuàng)建一個(gè)關(guān)系給一個(gè)用戶 user1 關(guān)注另一個(gè)用戶 user2抵碟,如下所示:
user1 = User.objects.get(id=1)
user2 = User.objects.get(id=2)
Contact.objects.create(user_from=user1, user_to=user2)
關(guān)系管理器 rel_form_set 和 rel_to_set 會(huì)返回一個(gè)查詢集(QuerySets)給Contace模型(model)桃漾。為了
從User模型(model)中存取最終的關(guān)系側(cè),Contace模型(model)會(huì)期望User包含一個(gè)ManyToManyField拟逮,如下所示(譯者注:以下代碼是作者假設(shè)的撬统,實(shí)際上User不會(huì)包含以下代碼):
following = models.ManyToManyField('self',
through=Contact,
related_name='followers',
symmetrical=False)
在這個(gè)例子中,我們告訴Django去使用我們定制的中介模型(intermediate model)來創(chuàng)建關(guān)系通過給ManyToManyField添加through=Contact
敦迄。這是一個(gè)從User模型到本身的多對(duì)對(duì)關(guān)系:我們?cè)?em>ManyToMnyfIELD字段中引用 'self'
來創(chuàng)建一個(gè)關(guān)系給相同的模型(model)恋追。
當(dāng)你在多對(duì)多關(guān)系中需要額外的字段,創(chuàng)建一個(gè)定制的模型(model)罚屋,一個(gè)關(guān)系側(cè)就是一個(gè)ForeignKey苦囱。添加一個(gè) ManyToManyField 在其中一個(gè)有關(guān)聯(lián)的模型(models)中然后通過在through參數(shù)中包含該中介模型(intermediate model)指示Django去使用你的定制中介模型(intermediate model)。
如果User模型(model)是我們應(yīng)用的一部分脾猛,我們可以添加以上的字段給模型(model)(譯者注:所以說撕彤,上面的代碼是作者假設(shè)存在)。但實(shí)際上猛拴,我們無法直接修改User類羹铅,因?yàn)樗菍儆?em>django.contrib.auth應(yīng)用的。我們將要做些輕微的改動(dòng)漆弄,給User模型動(dòng)態(tài)的添加這個(gè)字段睦裳。編輯account應(yīng)用中的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(譯者注:猴子補(bǔ)丁 Monkey patch 就是在運(yùn)行時(shí)對(duì)已有的代碼進(jìn)行修改廉邑,而不需要修改原始代碼)。你需要意識(shí)到倒谷,我們不推薦使用add_to_class()
為模型(models)添加字段蛛蒙。我們?cè)谶@個(gè)場景中利用這種方法是因?yàn)橐韵碌脑颍?/p>
- 我們可以非常簡單的取回關(guān)系對(duì)象使用Django ORM的
user.followers.all()
以及user.following.all()
。我們使用中介(intermediary) Contact 模型(model)可以避免復(fù)雜的查詢例如使用到額外的數(shù)據(jù)庫操作joins渤愁,如果在我們的定制Profile模型(model)中定義過了關(guān)系牵祟。 - 這個(gè)多對(duì)多關(guān)系的表將會(huì)被創(chuàng)建通過使用Contact模型(model)。因此抖格,動(dòng)態(tài)的添加ManyToManyField將不會(huì)對(duì)Django User 模型(model)的數(shù)據(jù)庫進(jìn)行任意改變诺苹。
- 我們避免了創(chuàng)建一個(gè)定義的用戶模型(model),保持了所有Django內(nèi)置User的特性雹拄。
請(qǐng)記住收奔,在大部分的場景中,在我們之前創(chuàng)建的Profile模型(model)添加字段是更好的方法滓玖,可以替代在User模型(model)上打上monkey-patch坪哄。Django還允許你使用定制的用戶模型(models)。如果你想要使用你的定制用戶模型(model),可以訪問 https://docs.djangoproject.com/en/1.8/topics/auth/customizing/#specifying-a-custom-user-model 獲得更多信息翩肌。
你能看到上述代碼中的關(guān)系包含了symmetrical=Flase
來定義一個(gè)非對(duì)稱(non-symmetric)關(guān)系模暗。這表示如果我關(guān)注了你,你不會(huì)自動(dòng)的關(guān)注我念祭。
當(dāng)你使用了一個(gè)中介模型(intermediate model)給多對(duì)多關(guān)系兑宇,一些關(guān)系管理器的方法將不可用,例如:
add()
粱坤,create()
以及remove()
顾孽。你需要?jiǎng)?chuàng)建或刪除中介模型(intermediate model)的實(shí)例來代替。
運(yùn)行如下命令來生成account應(yīng)用的初始遷移:
python manage.py makemigrations account
你會(huì)看到如下輸出:
Migrations for 'account':
0002_contact.py:
- Create model Contact
現(xiàn)在繼續(xù)運(yùn)行以下命令來同步應(yīng)用到數(shù)據(jù)庫中:
python manage.py migrate account
你會(huì)看到如下內(nèi)容包含在輸出中:
Applying account.0002_contact... OK
Contact模型(model)現(xiàn)在已經(jīng)被同步進(jìn)了數(shù)據(jù)庫比规,我們可以在用戶之間創(chuàng)建關(guān)系若厚。但是,我們的網(wǎng)站還沒有提供一個(gè)方法來瀏覽用戶或查看詳細(xì)的用戶profile蜒什。讓我們?yōu)?em>User模型構(gòu)建列表和詳情視圖(views)测秸。
為用戶profiles創(chuàng)建列表和詳情視圖(views)
打開account應(yīng)用中的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對(duì)象的簡單列表和詳情視圖(views)。user_list
視圖(view)獲得了所有的可用用戶灾常。Django User 模型(model)包含了一個(gè)標(biāo)志(flag)is_active
來指示用戶賬戶是否可用霎冯。我們通過is_active=True
來過濾查詢只返回可用的用戶。這個(gè)視圖(vies)返回了所有結(jié)果钞瀑,但是你可以改善它通過添加頁碼沈撞,這個(gè)方法我們?cè)?em>image_list視圖(view)中使用過。
user_detail視圖(view)使用get_object_or_404()
快捷方法來返回所有可用的用戶通過傳入的用戶名雕什。當(dāng)使用傳入的用戶名無法找到可用的用戶這個(gè)視圖(view)會(huì)返回一個(gè)HTTP 404響應(yīng)缠俺。
編輯account應(yīng)用的urls.py文件,為以上兩個(gè)視圖(views)添加URL模式贷岸,如下所示:
urlpatterns = [
# ...
url(r'^users/$', views.user_list, name='user_list'),
url(r'^users/(?P<username>[-\w]+)/$',
views.user_detail,
name='user_detail'),
]
我們會(huì)使用 user_detail
URL模式來給用戶生成規(guī)范的URL壹士。你之前就在模型(model)中定義了一個(gè)get_absolute_url()
方法來為每個(gè)對(duì)象返回規(guī)范的URL。另外一種方式為一個(gè)模型(model)指定一個(gè)URL是為你的項(xiàng)目添加ABSOLUTE_URL_OVERRIDES設(shè)置偿警。
編輯項(xiàng)目中的setting.py文件躏救,添加如下代碼:
ABSOLUTE_URL_OVERRIDES = {
'auth.user': lambda u: reverse_lazy('user_detail',
args=[u.username])
}
Django會(huì)為所有出現(xiàn)在ABSOLUTE_URL_OVERRIDES設(shè)置中的模型(models)動(dòng)態(tài)添加一個(gè)get_absolute_url()
方法。這個(gè)方法會(huì)給設(shè)置中指定的模型返回規(guī)范的URL螟蒸。我們給傳入的用戶返回user_detail URL『惺梗現(xiàn)在你可以在一個(gè)User實(shí)例上使用get_absolute_url()
來取回他自身的規(guī)范URL。打開Python shell輸入命令python manage.py shell
運(yùn)行以下代碼來進(jìn)行測試:
>>> from django.contrib.auth.models import User
>>> user = User.objects.latest('id')
>>> str(user.get_absolute_url())
'/account/users/ellington/'
返回的URL如同期望的一樣七嫌。我們需要為我們剛才創(chuàng)建的視圖(views)創(chuàng)建模板(templates)少办。在account應(yīng)用下的*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 %}
![]({{ im.url }})
{% endthumbnail %}
</a>
<div class="info">
<a href="{{ user.get_absolute_url }}" class="title">
{{ user.get_full_name }}
</a>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
這個(gè)模板(template)允許我們?cè)诰W(wǎng)站中排列所有可用的用戶。我們對(duì)給予的用戶進(jìn)行迭代并且使用`{% thumbnail %}模板(template)標(biāo)簽(tag)來生成profile圖片縮微圖抄瑟。
打開項(xiàng)目中的base.html模板(template)凡泣,在以下菜單項(xiàng)的href屬性中包含user_listURL:
<li {% if section == "people" %}class="selected"{% endif %}>
<a href="{% url "user_list" %}">People</a>
</li>
通過命令python manage.py runserver
啟動(dòng)開發(fā)服務(wù)器然后在瀏覽器打開 http://127.0.0.1:8000/account/users/ 。你會(huì)看到如下所示的用戶列:
(譯者注:圖靈皮假,特斯拉鞋拟,愛因斯坦,都是大牛叭亲省)
編輯account應(yīng)用下的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 %}
![]({{ im.url }})
{% 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="image-container">
{% include "images/image/list_ajax.html" with images=user.images_create.all %}
</div>
{% endwith %}
{% endblock %}
在詳情模板(template)中我們展示用戶profile并且我們使用{% thumbnail %}
模板(template)標(biāo)簽(tag)來顯示profile圖片。我們顯示粉絲的總數(shù)以及一個(gè)鏈接可以 follow/unfollow 該用戶褪测。我們會(huì)隱藏關(guān)注鏈接當(dāng)用戶在查看他們自己的profile猴誊,防止用戶自己關(guān)注自己。我們會(huì)執(zhí)行一個(gè)AJAX請(qǐng)求來 follow/unfollow 一個(gè)指定用戶侮措。我們給 <a>
HTML元素添加data-id和data-action屬性包含用戶ID以及當(dāng)該鏈接被點(diǎn)擊的時(shí)候會(huì)執(zhí)行的初始操作懈叹,follow/unfollow ,這個(gè)操作依賴當(dāng)前頁面的展示的用戶是否已經(jīng)被正在瀏覽的用戶所關(guān)注分扎。我們展示當(dāng)前頁面用戶的圖片書簽通過list_ajax.html模板澄成。
再次打開你的瀏覽器,點(diǎn)擊一個(gè)擁有圖片書簽的用戶鏈接畏吓,你會(huì)看到一個(gè)profile詳情如下所示:
創(chuàng)建一個(gè)AJAX視圖(view)來關(guān)注用戶
我們將會(huì)創(chuàng)建一個(gè)簡單的視圖(view)使用AJAX來 follow/unfollow 用戶墨状。編輯account應(yīng)用中的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)有點(diǎn)類似與我們之前創(chuàng)建的image_like視圖(view)。因?yàn)槲覀兪褂昧艘粋€(gè)定制中介模型(intermediate model)給用戶的多對(duì)多關(guān)系菲饼,所以ManyToManyField管理器默認(rèn)的add()
和remove()
方法將不可用肾砂。我們使用中介Contact模型(model)來創(chuàng)建或刪除用戶關(guān)系。
在account應(yīng)用中的urls.py文件中導(dǎo)入你剛才創(chuàng)建的視圖(view)然后為它添加URL模式:
url(r'^users/follow/$', views.user_follow, name='user_follow'),
請(qǐng)確保你放置的這個(gè)URL模式的位置在user_detailURL模式之前宏悦。否則镐确,任何對(duì) /users/follow/ 的請(qǐng)求都會(huì)被user_detail模式給正則匹配然后執(zhí)行。請(qǐng)記住饼煞,每一次的HTTP請(qǐng)求Django都會(huì)對(duì)每一條存在的URL模式進(jìn)行匹配直到第一條匹配成功才會(huì)停止繼續(xù)匹配辫塌。
編輯account應(yīng)用下的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代碼執(zhí)行AJAX請(qǐng)求來關(guān)注或不關(guān)注一個(gè)指定用戶并且觸發(fā) follow/unfollow 鏈接。我們使用jQuery去執(zhí)行AJAX請(qǐng)求的同時(shí)會(huì)設(shè)置 follow/unfollow 兩種鏈接的data-aciton屬性以及HTML<a>
元素的文本基于它上一次的值派哲。當(dāng)AJAX操作執(zhí)行完成臼氨,我們還會(huì)對(duì)顯示在頁面中的粉絲總數(shù)進(jìn)行更新。打開一個(gè)存在的用戶的詳情頁面芭届,然后點(diǎn)擊Follow鏈接嘗試下我們剛才構(gòu)建的功能是否正常储矩。
創(chuàng)建一個(gè)通用的活動(dòng)流(activity stream)應(yīng)用
許多社交網(wǎng)站會(huì)給他們的用戶顯示一個(gè)活動(dòng)流(activity stream),這樣他們可以跟蹤其他用戶在平臺(tái)中的操作褂乍。一個(gè)活動(dòng)流(activity stream)是一個(gè)用戶或一個(gè)用戶組最近活動(dòng)的列表持隧。舉個(gè)例子,Facebook的News Feed就是一個(gè)活動(dòng)流(activity stream)逃片。用戶X給Y圖片打上了書簽或者用戶X關(guān)注了用戶Y也是例子操作屡拨。我們將會(huì)構(gòu)建一個(gè)活動(dòng)流(activity stream)應(yīng)用這樣每個(gè)用戶都能看到他關(guān)注的用戶最近進(jìn)行的交互。為了做到上述功能,我們需要一個(gè)模型(modes)來保存用戶在網(wǎng)站上的操作執(zhí)行呀狼,還需要一個(gè)簡單的方法來添加操作給feed裂允。
運(yùn)行以下命令在你的項(xiàng)目中創(chuàng)建一個(gè)新的應(yīng)用命名為actions:
django-admin startapp actions
在你的項(xiàng)目中的settings.py文件中的INSTALLED_APPS設(shè)置中添加'actions',這樣可以讓Django知道這個(gè)新的應(yīng)用是可用狀態(tài):
INSTALLED_APPS = (
# ...
'actions',
)
編輯actions應(yīng)用下的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',)
這個(gè)Action模型(model)將會(huì)用來記錄用戶的活動(dòng)哥艇。模型(model)中的字段解釋如下:
- user:執(zhí)行該操作的用戶绝编。這個(gè)一個(gè)指向Django User模型(model)的 ForeignKey。
- verb:這是用戶執(zhí)行操作的動(dòng)作描述貌踏。
- created:這個(gè)時(shí)間日期會(huì)在動(dòng)作執(zhí)行的時(shí)候創(chuàng)建十饥。我們使用
auto_now_add=True
來動(dòng)態(tài)設(shè)置它為當(dāng)前的時(shí)間當(dāng)這個(gè)對(duì)象第一次被保存在數(shù)據(jù)庫中。
通過這個(gè)基礎(chǔ)模型(model)祖乳,我們只能夠存儲(chǔ)操作例如用戶X做了哪些事情逗堵。我們需要一個(gè)額外的ForeignKey字段為了保存操作會(huì)涉及到的一個(gè)target(目標(biāo))對(duì)象,例如用戶X給圖片Y打上了暑期那或者用戶X現(xiàn)在關(guān)注了用戶Y眷昆。就像你之前知道的砸捏,一個(gè)普通的ForeignKey只能指向一個(gè)其他的模型(model)。但是隙赁,我們需要一個(gè)方法垦藏,可以讓操作的target(目標(biāo))對(duì)象是任何一個(gè)已經(jīng)存在的模型(model)的實(shí)例。這個(gè)場景就由Django內(nèi)容類型框架來上演伞访。
使用內(nèi)容類型框架
Django包含了一個(gè)內(nèi)容類型框架位于django.contrib.contenttypes掂骏。這個(gè)應(yīng)用可以跟蹤你的項(xiàng)目中所有的模型(models)以及提供一個(gè)通用接口來與你的模型(models)進(jìn)行交互。
當(dāng)你使用startproject命令創(chuàng)建一個(gè)新的項(xiàng)目的時(shí)候這個(gè)contenttypes應(yīng)用就被默認(rèn)包含在INSTALLED_APPS設(shè)置中厚掷。它被其他的contrib包使用弟灼,例如認(rèn)證(authentication)框架以及admin應(yīng)用。
contenttypes應(yīng)用包含一個(gè)ContentType模型(model)冒黑。這個(gè)模型(model)的實(shí)例代表了你的應(yīng)用中真實(shí)存在的模型(models)田绑,并且新的ContentTYpe實(shí)例會(huì)動(dòng)態(tài)的創(chuàng)建當(dāng)新的模型(models)安裝在你的項(xiàng)目中。ContentType模型(model)有以下字段:
- app_label:模型(model)屬于的應(yīng)用名抡爹,它會(huì)自動(dòng)從模型(model)Meta選項(xiàng)中的app_label屬性獲取到掩驱。舉個(gè)例子:我們的Image模型(model)屬于images應(yīng)用
- model:模型(model)類的名字
- name:模型的可讀名,它會(huì)自動(dòng)從模型(model)Meta選項(xiàng)中的verbose_name獲取到冬竟。
讓我們看一下我們?nèi)绾螌?shí)例化ContentType對(duì)象欧穴。打開Python終端使用python manage.py shell
命令。你可以獲取一個(gè)指定模型(model)對(duì)應(yīng)的ContentType對(duì)象通過執(zhí)行一個(gè)帶有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)類從一個(gè)ContentType對(duì)象中通過調(diào)用它的model_class()
方法:
>>> from images.models import Image
>>> ContentType.objects.get_for_model(Image)
<ContentType: image>
以上就是內(nèi)容類型的一些例子涮帘。Django提供了更多的方法來使用他們進(jìn)行工作。你可以訪問 https://docs.djangoproject.com/en/1.8/ref/contrib/contenttypes/ 找到關(guān)于內(nèi)容類型框架的官方文檔笑诅。
添加通用的關(guān)系給你的模型(models)
在通用關(guān)系中ContentType對(duì)象扮演指向模型(model)的角色被關(guān)聯(lián)所使用调缨。你需要3個(gè)字段在模型(model)中組織一個(gè)通用關(guān)系:
- 一個(gè)ForeignKey字段ContentType疮鲫。這個(gè)字段會(huì)告訴我們給這個(gè)關(guān)聯(lián)的模型(model)。
- 一個(gè)字段用來存儲(chǔ)被關(guān)聯(lián)對(duì)象的primary key弦叶。這個(gè)字段通常是一個(gè)PositiveIntegerField用來匹配Django自動(dòng)的primary key字段俊犯。
- 一個(gè)字段用來定義和管理通用關(guān)系通過使用前面的兩個(gè)字段。內(nèi)容類型框架提供一個(gè)GenericForeignKey字段來完成這個(gè)目標(biāo)湾蔓。
編輯actions應(yīng)用的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模型添加了以下字段:
- target_ct:一個(gè)ForeignKey字段指向ContentType模型(model)砌梆。
- target_id:一個(gè)PositiveIntegerField用來存儲(chǔ)被關(guān)聯(lián)對(duì)象的primary key默责。
- target:一個(gè)GenericForeignKey字段指向被關(guān)聯(lián)的對(duì)象基于前面兩個(gè)字段的組合之上。
Django沒有創(chuàng)建任何字段在數(shù)據(jù)庫中給GenericForeignKey字段咸包。只有target_ct和target_id兩個(gè)字段被映射到數(shù)據(jù)庫字段桃序。兩個(gè)字段都有blank=True
和null=True
屬性所以一個(gè)target(目標(biāo))對(duì)象不是必須的當(dāng)保存Action對(duì)象的時(shí)候。
你可以讓你的應(yīng)用更加靈活通過使用通用關(guān)系替代外鍵當(dāng)它對(duì)擁有一個(gè)通用關(guān)系有意義烂瘫。
運(yùn)行以下命令來創(chuàng)建初始遷移為這個(gè)應(yīng)用:
python manage.py makemigrations actions
你會(huì)看到如下輸出:
Migrations for 'actions':
0001_initial.py:
- Create model Action
接著媒熊,運(yùn)行下一條命令來同步應(yīng)用到數(shù)據(jù)庫中:
python manage.py migrate
這條命令的輸出表明新的遷移已經(jīng)被應(yīng)用:
Applying actions.0001_initial... OK
讓我們?cè)诠芾碚军c(diǎn)中添加Action模型(model)。編輯actions應(yīng)用的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)
你已經(jīng)將Action模型(model)注冊(cè)到了管理站點(diǎn)中芦鳍。運(yùn)行命令python manage.py runserver
來初始化開發(fā)服務(wù)器然后在瀏覽器中打開 http://127.0.0.1:8000/admin/actions/action/add/ 。你會(huì)看到如下頁面可以創(chuàng)建一個(gè)新的Action對(duì)象:
如你所見葛账,只有target_ct和target_id兩個(gè)字段是映射為真實(shí)的數(shù)據(jù)庫字段顯示柠衅,并且GenericForeignKey字段不在這兒出現(xiàn)。target_ct允許你選擇任何一個(gè)在你的Django項(xiàng)目中注冊(cè)的模型(models)籍琳。你可以限制內(nèi)容類型從一個(gè)限制的模型(models)集合中選擇通過在target-ct字段中使用limit_choices_to屬性:limit_choices_to屬性允許你限制ForeignKey字段的內(nèi)容通過給予一個(gè)特定值的集合菲宴。
在actions應(yīng)用目錄下創(chuàng)建一個(gè)新的文件命名為utils.py。我們會(huì)定義一個(gè)快捷函數(shù)趋急,該函數(shù)允許我們使用一種簡單的方式創(chuàng)建新的Action對(duì)象喝峦。編輯這個(gè)新的文件添加如下代碼給它:
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()
函數(shù)允許我們創(chuàng)建actions,該actions可以包含一個(gè)target對(duì)象或不包含呜达。我們可以使用這個(gè)函數(shù)在我們代碼的任何地方添加新的actions給活動(dòng)流(activity stream)谣蠢。
在活動(dòng)流(activity stream)中避免重復(fù)的操作
有時(shí)候你的用戶可能多次執(zhí)行同個(gè)動(dòng)作。他們可能在短時(shí)間內(nèi)多次點(diǎn)擊 like/unlike 按鈕或者多次執(zhí)行同樣的動(dòng)作查近。這會(huì)導(dǎo)致你停止存儲(chǔ)和顯示重復(fù)的動(dòng)作漩怎。為了避免這種情況我們需要改善create_action()
函數(shù)來避免大部分的重復(fù)動(dòng)作。
編輯actions應(yīng)用中的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,
created__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()
函數(shù)來避免保存重復(fù)的動(dòng)作并且返回一個(gè)布爾值來告訴該動(dòng)作是否保存嗦嗡。下面來解釋我們是如何避免重復(fù)動(dòng)作的:
- 首先勋锤,我們通過Django提供的
timezone.now()
方法來獲取當(dāng)前時(shí)間。這個(gè)方法同datetime.datetime.now()相同侥祭,但是返回的是一個(gè)*timezone-aware*對(duì)象叁执。Django提供一個(gè)設(shè)置叫做*USE_TZ*用來啟用或關(guān)閉時(shí)區(qū)的支持茄厘。通過使用*startproject*命令創(chuàng)建的默認(rèn)*settings.py*包含
USE_TZ=True`。 - 我們使用last_minute變量來保存一分鐘前的時(shí)間谈宛,然后我們?nèi)』赜脩魪哪且院髨?zhí)行的任意一個(gè)相同操作次哈。
- 我們會(huì)創(chuàng)建一個(gè)Action對(duì)象如果在最后的一分鐘內(nèi)沒有存在同樣的動(dòng)作。我們會(huì)返回True如果一個(gè)Action對(duì)象被創(chuàng)建吆录,否則返回False窑滞。
添加用戶動(dòng)作給活動(dòng)流(activity stream)
是時(shí)候添加一些動(dòng)作給我們的視圖(views)來給我的用戶構(gòu)建活動(dòng)流(activity stream)了。我們將要存儲(chǔ)一個(gè)動(dòng)作為以下的每一個(gè)實(shí)例:
- 一個(gè)用戶給某張圖片打上書簽
- 一個(gè)用戶喜歡或不喜歡某張圖片
- 一個(gè)用戶創(chuàng)建一個(gè)賬戶
- 一個(gè)用戶關(guān)注或不關(guān)注某個(gè)用戶
編輯images應(yīng)用下的views.py文件添加以下導(dǎo)入:
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關(guān)系之后添加create_action()
撬槽,如下所示:
image.users_like.add(request.user)
create_action(request.user, 'likes', image)
現(xiàn)在編輯account應(yīng)用中的view.py文件添加以下導(dǎo)入:
from actions.utils import create_action
在register視圖(view)中此改,在創(chuàng)建Profile對(duì)象之后添加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)和我們的幫助函數(shù),現(xiàn)在保存新的動(dòng)作給活動(dòng)流(activity stream)是非常簡單的暂题。
顯示活動(dòng)流(activity stream)
最后移剪,我們需要一種方法來給每個(gè)用戶顯示活動(dòng)流(activity stream)。我們將會(huì)在用戶的dashboard中包含活動(dòng)流(activity stream)薪者。編輯account應(yīng)用的views.py文件挂滓。導(dǎo)入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})
在這個(gè)視圖(view),我們從數(shù)據(jù)庫取回所有的動(dòng)作(actions)啸胧,不包含當(dāng)前用戶執(zhí)行的動(dòng)作赶站。如果當(dāng)前用戶還沒有關(guān)注過任何人,我們展示在平臺(tái)中的其他用戶的最新動(dòng)作執(zhí)行纺念。這是一個(gè)默認(rèn)的行為當(dāng)當(dāng)前用戶還沒有關(guān)注過任何其他的用戶贝椿。如果當(dāng)前用戶已經(jīng)關(guān)注了其他用戶,我們就限制查詢只顯示當(dāng)前用戶關(guān)注的用戶的動(dòng)作執(zhí)行陷谱。最后烙博,我們限制結(jié)果只返回最前面的10個(gè)動(dòng)作。我們?cè)谶@兒并不使用order_by()
烟逊,因?yàn)槲覀円蕾囍耙呀?jīng)在Action模型(model)的Meta的排序選項(xiàng)渣窜。最新的動(dòng)作會(huì)首先返回,因?yàn)槲覀冊(cè)?em>Action模型(model)中設(shè)置過ordering = ('-created',)
宪躯。
優(yōu)化涉及被關(guān)聯(lián)的對(duì)想的查詢集(QuerySets)
每次你取回一個(gè)Aciton對(duì)象乔宿,你都可能存取它的有關(guān)聯(lián)的User對(duì)象,
并且可能這個(gè)用戶也關(guān)聯(lián)它的Profile對(duì)象访雪。Django ORM提供了一個(gè)簡單的方式一次性取回有關(guān)聯(lián)的對(duì)象详瑞,避免對(duì)數(shù)據(jù)庫進(jìn)行額外的查詢掂林。
使用select_related
Django提供了一個(gè)叫做select_related()
的查詢集(QuerySets)方法允許你取回關(guān)系為一對(duì)多的關(guān)聯(lián)對(duì)象。該方法將會(huì)轉(zhuǎn)化成一個(gè)單獨(dú)的坝橡,更加復(fù)雜的查詢集(QuerySets)泻帮,但是你可以避免額外的查詢當(dāng)存取這些關(guān)聯(lián)對(duì)象。select_relate方法是給ForeignKey和OneToOne字段使用的计寇。它通過執(zhí)行一個(gè) SQL JOIN并且包含關(guān)聯(lián)對(duì)象的字段在SELECT 聲明中锣杂。
為了利用select_related()
,編輯之前代碼中的以下行(譯者注:請(qǐng)注意雙下劃線):
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
(譯者注:請(qǐng)注意是雙下劃線)來連接profile表在一個(gè)單獨(dú)的SQL查詢中。如果你調(diào)用select_related()
而不傳入任何參數(shù)番宁,它會(huì)取回所有ForeignKey關(guān)系的對(duì)象元莫。給select_related()
限制的關(guān)系將會(huì)在隨后一直訪問。
小心的使用
select_related()
將會(huì)極大的提高執(zhí)行時(shí)間
使用prefetch_related
如你所見贝淤,select_related()
將會(huì)幫助你提高取回一對(duì)多關(guān)系的關(guān)聯(lián)對(duì)象的執(zhí)行效率柒竞。但是竭沫,select_related()
無法給多對(duì)多或者多對(duì)一關(guān)系(ManyToMany或者倒轉(zhuǎn)ForeignKey字段)工作有额。Django提供了一個(gè)不同的查詢集(QuerySets)方法叫做prefetch_realted效诅,該方法在select_related()
方法支持的關(guān)系上增加了多對(duì)多和多對(duì)一的關(guān)系。prefetch_related()
方法為每一種關(guān)系執(zhí)行單獨(dú)的查找然后對(duì)各個(gè)結(jié)果進(jìn)行連接通過使用Python离陶。這個(gè)方法還支持GeneriRelation和GenericForeignKey的預(yù)先讀取。
完成你的查詢通過為它添加prefetch_related()
給目標(biāo)GenericForeignKey字段衅檀,如下所示:
actions = actions.filter(user_id__in=following_ids)\
.select_related('user', 'user__profile')\
.prefetch_related('target')
這個(gè)查詢現(xiàn)在已經(jīng)被充分利用用來取回包含關(guān)聯(lián)對(duì)象的用戶動(dòng)作(actions)招刨。
為actions創(chuàng)建模板(templates)
我們要?jiǎng)?chuàng)建一個(gè)模板(template)用來顯示一個(gè)獨(dú)特的Action對(duì)象。在actions應(yīng)用中創(chuàng)建一個(gè)新的目錄命名為templates哀军。添加如下文件結(jié)構(gòu):
actions/
action/
detail.html
編輯actions/action/detail.html模板(template)文件添加如下代碼:
明天添加
這個(gè)模板用來顯示一個(gè)Action對(duì)象沉眶。首先,我們使用{% with %}
模板標(biāo)簽(template tag)來獲取用戶操作的動(dòng)作(action)和他們的profile杉适。然后谎倔,我們顯示目標(biāo)對(duì)象的圖片如果Action對(duì)象有一個(gè)關(guān)聯(lián)的目標(biāo)對(duì)象。最后猿推,如果有執(zhí)行過的動(dòng)作(action)片习,包括動(dòng)作和目標(biāo)對(duì)象,我們就顯示鏈接給用戶蹬叭。
現(xiàn)在藕咏,編輯account/dashboard.html模板(template)添加如下代碼到content區(qū)塊下方:
<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/ 。登錄一個(gè)存在的用戶并且該用戶執(zhí)行過一些操作已經(jīng)被存儲(chǔ)在數(shù)據(jù)庫中秽五。然后孽查,登錄其他用戶,關(guān)注之前登錄的用戶坦喘,在dashboard頁面可以看到生成的動(dòng)作流卦碾。如下所示:
我們剛剛創(chuàng)建了一個(gè)完整的活動(dòng)流(activity stream)給我們的用戶并且我們還能非常容易的添加新的用戶動(dòng)作給它铺坞。你還可以添加無限的滾動(dòng)功能給活動(dòng)流(activity stream)通過集成AJAX分頁處理,和我們之前在image_list視圖(view)使用過的一樣洲胖。
給非規(guī)范化(denormalizing)計(jì)數(shù)使用信號(hào)
有一些場景济榨,你想要使你的數(shù)據(jù)非規(guī)范化。非規(guī)劃化使指在一定的程度上制造一些數(shù)據(jù)冗余用來優(yōu)化讀取的性能绿映。你必須十分小心的使用非規(guī)劃化并且只有在你真的非常需要它的時(shí)候才能使用擒滑。你會(huì)發(fā)現(xiàn)非規(guī)劃化的最大問題就是保持你的非規(guī)范化數(shù)據(jù)更新是非常困難的。
我們將會(huì)看到一個(gè)例子關(guān)于如何改善(improve)我們的查詢通過使用非規(guī)范化計(jì)數(shù)叉弦。缺點(diǎn)就是我們不得不保持冗余數(shù)據(jù)的更新丐一。我們將要從我們的Image模型(model)中使數(shù)據(jù)非規(guī)范化然后使用Django信號(hào)來保持?jǐn)?shù)據(jù)的更新。
使用信號(hào)進(jìn)行工作
Django自帶一個(gè)信號(hào)調(diào)度程序允許receiver函數(shù)在某個(gè)動(dòng)作出現(xiàn)的時(shí)候去獲取通知淹冰。信號(hào)非常有用库车,當(dāng)你需要你的代碼去執(zhí)行某些事件的時(shí)候同時(shí)正在發(fā)生其他事件。你還能夠創(chuàng)建你自己的信號(hào)這樣一來其他人可以在某個(gè)事件發(fā)生的時(shí)候獲得通知樱拴。
Django模型(models)提供了幾個(gè)信號(hào)柠衍,它們位于django.db.models.signales。舉幾個(gè)例子:
-
pre_save 和 post_save:前者會(huì)在調(diào)用模型(model)的
save()
方法前發(fā)送信號(hào)晶乔,后者反之珍坊。 -
pre_delete 和 post_delete:前者會(huì)在調(diào)用模型(model)或查詢集(QuerySets)的
delete()
方法之前發(fā)送信號(hào),后者反之正罢。 - m2m_changed:當(dāng)在一個(gè)模型(model)上的ManayToManayField被改變的時(shí)候發(fā)送信號(hào)阵漏。
以上只是Django提供的一小部分信號(hào)。你可以通過訪問 https://docs.djangoproject.com/en/1.8/ref/signals/ 獲得更多信號(hào)資料翻具。
打個(gè)比方履怯,你想要獲取熱門圖片。你可以使用Django的聚合函數(shù)來獲取圖片裆泳,通過圖片獲取的用戶喜歡數(shù)量來進(jìn)行排序叹洲。要記住你已經(jīng)使用過Django聚合函數(shù)在第三章 擴(kuò)展你的blog應(yīng)用。以下代碼將會(huì)獲取圖片并進(jìn)行排序通過它們被用戶喜歡的數(shù)量:
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')
但是晾虑,通過統(tǒng)計(jì)圖片的總喜歡數(shù)量進(jìn)行排序比直接使用一個(gè)已經(jīng)存儲(chǔ)總統(tǒng)計(jì)數(shù)的字段進(jìn)行排序要消耗更多的性能疹味。你可以添加一個(gè)字段給Image模型(model)用來非規(guī)范化喜歡的數(shù)量用來提升涉及該字段的查詢的性能。那么帜篇,問題來了糙捺,我們?cè)撊绾伪3诌@個(gè)字段是最新更新過的。
編輯images應(yīng)用下的models.py文件笙隙,給Image模型(model)添加以下字段:
total_likes = models.PositiveIntegerField(db_index=True,
default=0)
total_likes字段允許我們給每張圖片存儲(chǔ)被用戶喜歡的總數(shù)洪灯。非規(guī)范化數(shù)據(jù)非常有用當(dāng)你想要使用他們來過濾或排序查詢集(QuerySets)。
在你使用非規(guī)范化字段之前你必須考慮下其他幾種提高性能的方法竟痰∏┕常考慮下數(shù)據(jù)庫索引掏呼,最佳化查詢以及緩存在開始規(guī)范化你的數(shù)據(jù)之前。
運(yùn)行以下命令將新添加的字段遷移到數(shù)據(jù)庫中:
python manage.py makemigrations images
你會(huì)看到如下輸出:
Migrations for 'images':
0002_image_total_likes.py:
- Add field total_likes to image
接著繼續(xù)運(yùn)行以下命令來應(yīng)用遷移:
python manage.py migrate images
輸出中會(huì)包含以下內(nèi)容:
Applying images.0002_image_total_likes... OK
我們要給m2m_changed信號(hào)附加一個(gè)receiver函數(shù)铅檩。在images應(yīng)用目錄下創(chuàng)建一個(gè)新的文件命名為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
函數(shù)注冊(cè)成一個(gè)receiver函數(shù)昧旨,然后我們將該函數(shù)附加給m2m_changed信號(hào)拾给。我們將這個(gè)函數(shù)與Image.users_like.through連接,這樣這個(gè)函數(shù)只有當(dāng)m2m_changed信號(hào)被Image.users_like.through執(zhí)行的時(shí)候才被調(diào)用兔沃。還有一個(gè)可以替代的方式來注冊(cè)一個(gè)receiver函數(shù)蒋得,由使用Signal對(duì)象的connect()
方法組成。
Django信號(hào)是同步阻塞的乒疏。不要使用異步任務(wù)導(dǎo)致信號(hào)混亂额衙。但是,你可以聯(lián)合兩者來執(zhí)行異步任務(wù)當(dāng)你的代碼只接受一個(gè)信號(hào)的通知怕吴。
你必須連接你的receiver函數(shù)給一個(gè)信號(hào)窍侧,只有這樣它才會(huì)被調(diào)用當(dāng)連接的信號(hào)發(fā)送的時(shí)候。有一個(gè)推薦的方法用來注冊(cè)你的信號(hào)是在你的應(yīng)用配置類中導(dǎo)入它們到ready()
方法中械哟。Django提供一個(gè)應(yīng)用注冊(cè)允許你對(duì)你的應(yīng)用進(jìn)行配置和內(nèi)省疏之。
典型的應(yīng)用配置類
django允許你指定配置類給你的應(yīng)用們殿雪。為了提供一個(gè)自定義的配置給你的應(yīng)用暇咆,創(chuàng)建一個(gè)繼承django.apps的Appconfig類的自定義類。這個(gè)應(yīng)用配置類允許你為應(yīng)用存儲(chǔ)元數(shù)據(jù)和配置并且提供
內(nèi)省丙曙。
你可以通過訪問 https://docs. djangoproject.com/en/1.8/ref/applications/ 獲取更多關(guān)于應(yīng)用配置的信息爸业。
為了注冊(cè)你的信號(hào)receiver函數(shù),當(dāng)你使用receiver()
裝飾器的時(shí)候亏镰,你只需要導(dǎo)入信號(hào)模塊扯旷,這些信號(hào)模塊被包含在你的應(yīng)用的AppConfig類中的ready()
方法中。這個(gè)方法在應(yīng)用注冊(cè)被完整填充的時(shí)候就調(diào)用索抓。其他給你應(yīng)用的初始化都可以被包含在這個(gè)方法中钧忽。
在images應(yīng)用目錄下創(chuàng)建一個(gè)新的文件命名為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屬性定義該應(yīng)用完整的Python路徑逼肯。verbose_name屬性設(shè)置了這個(gè)應(yīng)用可讀的名字耸黑。它會(huì)在管理站點(diǎn)中顯示。ready()
方法就是我們?yōu)檫@個(gè)應(yīng)用導(dǎo)入信號(hào)的地方篮幢。
現(xiàn)在我們需要告訴Django我們的應(yīng)用配置位于哪里大刊。編輯位于images應(yīng)用目錄下的init.py文件添加如下內(nèi)容:
default_app_config = 'images.apps.ImagesConfig'
打開你的瀏覽器瀏覽一個(gè)圖片的詳細(xì)頁面然后點(diǎn)擊like按鈕。再進(jìn)入管理頁面看下該圖片的total_like屬性三椿。你會(huì)看到total_likes屬性已經(jīng)更新了最新的like數(shù)如下所示:
現(xiàn)在缺菌,你可以使用totla_likes屬性來進(jìn)行熱門圖片的排序或者在任何地方顯示這個(gè)值葫辐,從而避免了復(fù)雜的查詢操作。以下獲取圖片的查詢通過圖片的喜歡數(shù)量進(jìn)行排序:
images_by_popularity = Image.objects.annotate(
likes=Count('users_like')).order_by('-likes')
現(xiàn)在我們可以用新的查詢來代替上面的查詢:
images_by_popularity = Image.objects.order_by('-total_likes')
以上查詢的返回結(jié)果只需要很少的SQL查詢性能伴郁。以上就是一個(gè)例子關(guān)于如何使用Django信號(hào)耿战。
小心使用信號(hào),因?yàn)樗鼈儠?huì)給理解控制流制造困難焊傅。在很多場景下你可以避免使用信號(hào)如果你知道哪個(gè)接收器需要被通知昆箕。
使用Redis來存儲(chǔ)視圖(views)項(xiàng)
Redis是一個(gè)高級(jí)的key-value(鍵值)數(shù)據(jù)庫允許你保存不同類型的數(shù)據(jù)并且在I/O(輸入/輸出)操作上非常非常的快速。Redis可以在內(nèi)存中存儲(chǔ)任何東西租冠,但是這些數(shù)據(jù)能夠持續(xù)通過偶爾存儲(chǔ)數(shù)據(jù)集到磁盤中或者添加每一條命令到日志中鹏倘。Redis是非常出彩的通過與其他的鍵值存儲(chǔ)對(duì)比:它提供了一個(gè)強(qiáng)大的設(shè)置命令,并且支持多種數(shù)據(jù)結(jié)構(gòu)顽爹,例如string纤泵,hashes,lists镜粤,sets捏题,ordered sets,甚至bitmaps和HyperLogLogs肉渴。
SQL最適合用于模式定義的持續(xù)數(shù)據(jù)存儲(chǔ)公荧,而Redis提供了許多優(yōu)勢(shì)當(dāng)需要處理快速變化的數(shù)據(jù),易失性存儲(chǔ)同规,或者需要一個(gè)快速緩存的時(shí)候循狰。讓我們看下Redis是如何被使用的,當(dāng)構(gòu)建新的功能到我們的項(xiàng)目中券勺。
安裝Redis
從 http://redis.io/download 下載最新的Redis版本绪钥。解壓tar.gz文件,進(jìn)入redis目錄然后編譯Redis通過使用以下make命令:
cd redis-3.0.4(譯者注:版本根據(jù)自己下載的修改)
make (譯者注:這里是假設(shè)你使用的是linux或者mac系統(tǒng)才有make命令关炼,windows如何操作請(qǐng)看下官方文檔)
在Redis安裝完成后允許以下shell命令來初始化Redis服務(wù):
src/redis-server
你會(huì)看到輸出的結(jié)尾如下所示:
# 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
默認(rèn)的程腹,Redis運(yùn)行會(huì)占用6379端口,但是你也可以指定一個(gè)自定義的端口通過使用--port
標(biāo)志儒拂,例如:redis-server --port 6655
寸潦。當(dāng)你的服務(wù)啟動(dòng)完畢,你可以在其他的終端中打開Redis客戶端通過使用如下命令:
src/redis-cli
你會(huì)看到Redis客戶端shell如下所示:
127.0.0.1:6379>
Redis客戶端允許你在當(dāng)前shell中立即執(zhí)行Rdis命令社痛。來我們來嘗試一些命令见转。鍵入SET命令在Redis客戶端中存儲(chǔ)一個(gè)值到一個(gè)鍵中:
127.0.0.1:6379> SET name "Peter"
ok
以上的命令創(chuàng)建了一個(gè)帶有字符串“Peter”值的name鍵到Redis數(shù)據(jù)庫中。OK輸出表明該鍵已經(jīng)被成功保存褥影。然后池户,使用GET命令獲取之前的值,如下所示:
127.0.0.1:6379> GET name
"Peter"
你還可以檢查一個(gè)鍵是否存在通過使用EXISTS命令。如果檢查的鍵存在會(huì)返回1校焦,反之返回0:
127.0.0.1:6379> EXISTS name
(integer) 1
你可以給一個(gè)鍵設(shè)置到期時(shí)間通過使用EXPIRE命令赊抖,該命令允許你設(shè)置該鍵能在幾秒內(nèi)存在。另一個(gè)選項(xiàng)使用EXPIREAT命令來期望一個(gè)Unix時(shí)間戳寨典。鍵的到期消失是非常有用的當(dāng)將Redis當(dāng)做緩存使用或者存儲(chǔ)易失性的數(shù)據(jù):
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)響應(yīng)是一個(gè)空的響應(yīng)說明沒有找到鍵氛雪。你還可以通過使用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)
以上只是一些鍵選項(xiàng)的基本命令耸成。Redis包含了龐大的命令設(shè)置給一些數(shù)據(jù)類型报亩,例如strings,hashes井氢,sets弦追,ordered sets等等。你可以通過訪問 http://redis.io/commands 看到所有Reids命令以及通過訪問 http://redis.io/topics/data-types 看到所有Redis支持的數(shù)據(jù)類型花竞。
通過Python使用Redis
我們需要綁定Python和Redis劲件。通過pip渠道安裝redis-py命令如下:
pip install redis==2.10.3(譯者注:版本可能有更新,如果需要最新版本约急,可以不帶上'==2.10.3'后綴)
你可以訪問 http://redis-py.readthedocs.org/ 得到redis-py文檔零远。
redis-py提供兩個(gè)類用來與Redis交互:StrictRedis和Redis。兩者提供了相同的功能厌蔽。StrictRedis類嘗試遵守官方的Redis命令語法牵辣。Redis類型繼承Strictredis重寫了部分方法來提供向后的兼容性。我們將會(huì)使用StrictRedis類奴饮,因?yàn)樗袷豏edis命令語法纬向。打開Python shell執(zhí)行以下命令:
>>> import redis
>>> r = redis.StrictRedis(host='localhost', port=6379, db=0)
上面的代碼創(chuàng)建了一個(gè)與Redis數(shù)據(jù)庫的連接。在Redis中拐云,數(shù)據(jù)庫通過一個(gè)整形索引替代數(shù)據(jù)庫名字來辨識(shí)罢猪。默認(rèn)的近她,一個(gè)客戶端被連接到數(shù)據(jù)庫 0 叉瘩。Reids數(shù)據(jù)庫可用的數(shù)字設(shè)置到16,但是你可以在redis.conf文件中修改這個(gè)值粘捎。
現(xiàn)在使用Python shell設(shè)置一個(gè)鍵:
>>> r.set('foo', 'bar')
True
以上命令返回Ture表明這個(gè)鍵已經(jīng)創(chuàng)建成功∞泵澹現(xiàn)在你可以使用get()
命令取回該鍵:
>>> r.get('foo')
'bar'
如你所見,StrictRedis方法遵守Redis命令語法攒磨。
讓我們集成Rdies到我們的項(xiàng)目中泳桦。編輯bookmarks項(xiàng)目的settings.py文件添加如下設(shè)置:
REDIS_HOST = 'localhost'
REDIS_PORT = 6379
REDIS_DB = 0
以上設(shè)置了Redis服務(wù)器和我們將要在項(xiàng)目中使用到的數(shù)據(jù)庫。
存儲(chǔ)視圖(vies)項(xiàng)到Redis中
讓我們存儲(chǔ)一張圖片被查看的總次數(shù)娩缰。如果我們通過Django ORM來完成這個(gè)操作灸撰,它會(huì)在每次該圖片顯示的時(shí)候執(zhí)行一次SQL UPDATE聲明。使用Redis,我們只需要對(duì)一個(gè)計(jì)數(shù)器進(jìn)行增量存儲(chǔ)在內(nèi)存中浮毯,從而帶來更好的性能完疫。
編輯images應(yīng)用下的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})
在這個(gè)視圖(view)中壳鹤,我們使用INCR命令,它會(huì)從1開始增量一個(gè)鍵的值饰迹,在執(zhí)行這個(gè)操作之前如果鍵不存在芳誓,它會(huì)將值設(shè)定為0.incr()
方法在執(zhí)行操作后會(huì)返回鍵的值,然后我們可以存儲(chǔ)該值到total_views變量中啊鸭。我們構(gòu)建Rddis鍵使用一個(gè)符號(hào)锹淌,比如 object-type:id:field (for example image:33:id) 。
對(duì)Redis的鍵進(jìn)行命名有一個(gè)慣例是使用冒號(hào)進(jìn)行分割來創(chuàng)建鍵的命名空間赠制。做到這點(diǎn)葛圃,鍵的名字會(huì)特別冗長,有關(guān)聯(lián)的鍵會(huì)分享部分相同的模式在它們的名字中憎妙。
編輯image/detail.html模板(template)在已有的<span class="count">
元素之后添加如下代碼:
<span class="count">
<span class="total">{{ total_views }}</span>
view{{ total_views|pluralize }}
</span>
現(xiàn)在在瀏覽器中打開一張圖片的詳細(xì)頁面然后多次加載該頁面库正。你會(huì)看到每次該視圖(view)被執(zhí)行的時(shí)候,總的觀看次數(shù)會(huì)增加 1 厘唾。如下所示:
你已經(jīng)成功的集成Redis到你的項(xiàng)目中來存儲(chǔ)項(xiàng)統(tǒng)計(jì)褥符。
存儲(chǔ)一個(gè)排名到Reids中
讓我們使用Reids構(gòu)建更多的功能。我們要在我們的平臺(tái)中創(chuàng)建一個(gè)最多瀏覽次數(shù)的圖片排行抚垃。為了構(gòu)建這個(gè)排行我們將要使用Redis分類集合喷楣。一個(gè)分類集合是一個(gè)非重復(fù)的字符串采集,其中每個(gè)成員和一個(gè)分?jǐn)?shù)關(guān)聯(lián)鹤树。其中的項(xiàng)根據(jù)它們的分?jǐn)?shù)進(jìn)行排序铣焊。
編輯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()
命令存儲(chǔ)圖片視圖(views)到一個(gè)分類集合中通過鍵image:ranking
罕伯。我們存儲(chǔ)圖片id曲伊,和一個(gè)分?jǐn)?shù)1,它們將會(huì)被加到分類集合中這個(gè)元素的總分上追他。這將允許我們?cè)谌稚铣掷m(xù)跟蹤所有的圖片視圖(views)坟募,并且有一個(gè)分類集合,該分類集合通過圖片的瀏覽次數(shù)進(jìn)行排序邑狸。
現(xiàn)在創(chuàng)建一個(gè)新的視圖(view)用來展示最多瀏覽次數(shù)圖片的排行懈糯。在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()
命令獲得分類集合中的元素单雾。這個(gè)命令期望一個(gè)自定義的范圍赚哗,最低分和最高分她紫。通過將 0 定為最低分, -1 為最高分屿储,我們告訴Redis返回分類集合中的所有元素犁苏。最終,我們使用[:10]
對(duì)結(jié)果進(jìn)行切片獲取最前面十個(gè)最高分的元素扩所。我們構(gòu)建一個(gè)返回的圖片IDs的列围详,然后我們將該列存儲(chǔ)在image_ranking_ids變量中,這是一個(gè)整數(shù)列祖屏。我們通過這些IDs取回對(duì)應(yīng)的Image對(duì)象助赞,并將它們強(qiáng)制轉(zhuǎn)化為列通過使用list()
函數(shù)。強(qiáng)制轉(zhuǎn)化查詢集(QuerySets)的執(zhí)行是非常重要的袁勺,因?yàn)榻酉聛砦覀円谠摿猩鲜褂昧械?code>sort()方法(就是因?yàn)檫@點(diǎn)所以我們需要的是一個(gè)對(duì)象列而不是一個(gè)查詢集(QuerySets))雹食。我們排序這些Image對(duì)象通過它們?cè)趫D片排行中的索引。現(xiàn)在我們可以在我們的模板(template)中使用most_viewed列來顯示10個(gè)最多瀏覽次數(shù)的圖片期丰。
創(chuàng)建一個(gè)新的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 %}
這個(gè)模板(template)非常簡單明了,我們只是對(duì)包含在most_viewed中的Image對(duì)象進(jìn)行迭代钝荡。
最后為新的視圖(view)創(chuàng)建一個(gè)URL模式街立。編輯images應(yīng)用下的urls.py文件,添加如下內(nèi)容:
url(r'^ranking/$', views.image_ranking, name='create'),
在瀏覽器中打開 http://127.0.0.1:8000/images/ranking/ 埠通。你會(huì)看到如下圖片排行:
Redis的下一步
Redis并不能替代你的SQL數(shù)據(jù)庫赎离,但是它是一個(gè)內(nèi)存中的快速存儲(chǔ),更適合某些特定任務(wù)端辱。將它添加到你的棧中使用當(dāng)你真的感覺它很需要梁剔。以下是一些適合Redis的場景:
- Counting:如你之前看到的,通過Redis管理計(jì)數(shù)器非常容易舞蔽。你可以使用
incr()
和`incrby()荣病。 - Storing latest items:你可以添加項(xiàng)到一個(gè)列的開頭和結(jié)尾通過使用
lpush()
和rpush()
。移除和返回開頭和結(jié)尾的元素通過使用lpop()
以及rpop()
渗柿。你可以削減列的長度通過使用ltrim()
來維持它的長度个盆。 - Queues:除了push和pop命令,Redis還提供堵塞的隊(duì)列命令做祝。
- Caching:使用
expire()
和expireat()
允許你將Redis當(dāng)成緩存使用砾省。你還可以找到第三方的Reids緩存后臺(tái)給Django使用。 - Pub/Sub:Redis提供命令給訂閱或不訂閱混槐,并且給渠道發(fā)送消息。
- Rankings and leaderboards:Redis使用分?jǐn)?shù)的分類集合使創(chuàng)建排行榜非常的簡單轩性。
- Real-time tracking:Redis快速的I/O(輸入/輸出)使它能完美支持實(shí)時(shí)場景声登。
總結(jié)
在本章中,你構(gòu)建了一個(gè)粉絲系統(tǒng)和一個(gè)用戶活動(dòng)流(activity stream)。你學(xué)習(xí)了Django信號(hào)是如何進(jìn)行工作并且在你的項(xiàng)目中集成了Redis悯嗓。
在下一章中件舵,你會(huì)學(xué)習(xí)到如何構(gòu)建一個(gè)在線商店。你會(huì)創(chuàng)建一個(gè)產(chǎn)品目錄并且通過會(huì)話(sessions)創(chuàng)建一個(gè)購物車脯厨。你還會(huì)學(xué)習(xí)如何通過Celery執(zhí)行異步任務(wù)铅祸。
譯者總結(jié):
這一章好長啊合武!最后部分的Redis感覺最實(shí)用临梗。準(zhǔn)備全書翻譯好后再抽時(shí)間把翻譯好的所有章節(jié)全部重新校對(duì)下!那么大家下章再見稼跳!祈禱我年終中大獎(jiǎng)盟庞!