點(diǎn)我查看本文集的說(shuō)明及目錄奴曙。
本項(xiàng)目相關(guān)內(nèi)容( github傳送 )包括:
實(shí)現(xiàn)過(guò)程:
項(xiàng)目總結(jié)及改進(jìn):
網(wǎng)站應(yīng)用實(shí)現(xiàn)微信登錄
CH6 關(guān)注用戶動(dòng)態(tài)
上一章椎侠,我們使用 jQuery 為項(xiàng)目添加 AJAX 視圖,并創(chuàng)建了將其它網(wǎng)站的內(nèi)容分享到自己平臺(tái)上的 JavaScript bookmarklet堤结。
本章,我們將學(xué)習(xí)如何創(chuàng)建關(guān)注系統(tǒng)和用戶活動(dòng)流。我們將了解 Django signal 如何工作治专,集成 Redis 快速 I/O 存儲(chǔ)來(lái)保存視圖。
本章將包含以下內(nèi)容:
使用中間模型創(chuàng)建多對(duì)多關(guān)系
創(chuàng)建 AJAX 視圖
創(chuàng)建活動(dòng)流應(yīng)用
為模型添加通用關(guān)系
為相關(guān)對(duì)象優(yōu)化 QuerySets
使用 signals 進(jìn)行非標(biāo)準(zhǔn)化計(jì)數(shù)
-
在 Redis 中保存視圖
?
創(chuàng)建關(guān)注系統(tǒng)
我們將在項(xiàng)目中創(chuàng)建關(guān)注系統(tǒng)遭顶。用戶可以互相關(guān)注并看到其他用戶在自己平臺(tái)上分享的內(nèi)容张峰。用戶之間是多對(duì)多關(guān)系,一個(gè)用戶可以關(guān)注多個(gè)用戶棒旗,也可以被多個(gè)用戶關(guān)注喘批。
通過(guò)中間模型創(chuàng)建多對(duì)多模型
上一章,我們已經(jīng)使用 ManyToManyField 創(chuàng)建相關(guān)模型的多對(duì)多關(guān)系嗦哆,這樣 Django 將自動(dòng)為關(guān)系創(chuàng)建數(shù)據(jù)庫(kù)模型谤祖。這種方法適合大多數(shù)情況,但有時(shí)我們也需要為關(guān)系創(chuàng)建中間模型老速。中間模型用來(lái)為關(guān)系添加其它內(nèi)容(比如關(guān)系創(chuàng)建日期或者描述關(guān)系類型的字段)粥喜。
我們將創(chuàng)建中間模型來(lái)建立用戶之間的關(guān)系。這里創(chuàng)建中間模型的原因有兩個(gè):
使用 Django 提供的 user 模型保存用戶信息橘券,要盡量避免對(duì)其進(jìn)行更改额湘;
保存關(guān)系創(chuàng)建的時(shí)間。
編輯 account 應(yīng)用的 models.py 文件并添加以下代碼:
class Contact(models.Model):
user_from = models.ForeignKey(settings.AUTH_USER_MODEL,
related_name='rel_from_set')
user_to = models.ForeignKey(settings.AUTH_USER_MODEL,
related_name='rel_to_set')
created = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta:
ordering = ('-created',)
def __str__(self):
return '{} flollows {}'.format(self.user_from, self.user_to)
筆者注:
為了與第四章的內(nèi)容保持一致旁舰,這里使用 settings.AUTH_USER_MODEL 代替了 User锋华。
這是表示用戶關(guān)系的 Contact 模型。它包含以下字段:
- user_from: 表示創(chuàng)建關(guān)系用戶的 ForeignKey 箭窜;
- user_to: 表示被關(guān)注用戶的 ForeignKey 毯焕;
- created: auto_now_add 設(shè)置為 True 的 DateTimeField 字段,用于保存關(guān)系創(chuàng)建的時(shí)間。
ForeignKey 字段將自動(dòng)創(chuàng)建數(shù)據(jù)庫(kù)索引纳猫。這里使用 db_index=True 為 created 字段創(chuàng)建索引婆咸。如果 QuerySets 使用該字段進(jìn)行排序,創(chuàng)建索引將提高查詢效率芜辕。
通過(guò) ORM 尚骄,我們可以創(chuàng)建 user1 關(guān)注其它用戶 user2 的關(guān)系:
user1 = User.objects.get(id=1)
user2 = User.objects.get(id=2)
Contact.objects.create(user_from=user1, user_to=user2)
管理器 rel_from_set 和 rel_to_set 將為 Contact 模型返回 queryset 。我們可以在 User 模型中增加下面的 ManyToManyField 字段來(lái)訪問(wèn)關(guān)系的另一端:
following = models.ManyToManyField('self',through=Contact,related_name='followers',symmetrical=False)
我們使用上面的代碼為 ManyToManyField 設(shè)置 through=Contact 來(lái)使用自定義中間模型 Contact 侵续。這是一個(gè) User 模型本身的多對(duì)多關(guān)系倔丈;通過(guò) ManyToManyField 指向 self 來(lái)為相同模型創(chuàng)建關(guān)系。
注意:
如果需要在多對(duì)多關(guān)系中添加額外字段状蜗,那么請(qǐng)創(chuàng)建一個(gè)包含關(guān)系兩端 ForeignKey 的自定義模型需五,并在相關(guān)模型的一個(gè)模型中添加一個(gè) ManyToManyField 并通過(guò) through 參數(shù)設(shè)置中間模型。
如果 User 模型是應(yīng)用的一部分诗舰,我們可以這樣向模型添加上面的字段警儒。然而, User 模型屬于 django.contrib.auth 應(yīng)用眶根,我們不能直接對(duì)其進(jìn)行更改蜀铲。這里我們動(dòng)態(tài)向 User 模型添加字段,編輯 accounts 應(yīng)用的 models.py 文件并添加以下內(nèi)容:
from django.contrib import auth
auth.get_user_model().add_to_class('following', models.ManyToManyField('self',
through=Contact,
related_name='followers',
symmetrical=False))
筆者注:
為了與第四章的內(nèi)容保持一致,這里使用 auth.get_user_model() 代替 User。
上面的代碼使用 Django 模型的 add_to_class() 方法來(lái)對(duì) User 模型打補(bǔ)丁( monkey-patch) 慢叨。Django 并不推薦使用 add_to_class() 為模型添加字段,我們?cè)谶@里使用它的原因在于:
通過(guò) user.followers.all() 和 user.followering.all() 簡(jiǎn)化了獲取相關(guān)對(duì)象的方法厌丑。就像我們?cè)谧远x模型 Profile 中定義關(guān)系一樣 ,這里使用中間模型 Contact 來(lái)避免額外連接的復(fù)雜查詢渔呵。
多對(duì)多關(guān)系的數(shù)據(jù)庫(kù)表將通過(guò) Contact 模型進(jìn)行創(chuàng)建怒竿。這樣動(dòng)態(tài)添加 ManyToManyField 不會(huì)對(duì) User 模型產(chǎn)生影響。
避免創(chuàng)建自定義用戶模型扩氢,保留了 Django 內(nèi)置 User 模型的所有優(yōu)勢(shì)耕驰。
在大多數(shù)情況下,與其對(duì) User 模型打補(bǔ)堵疾颉(monkey-patching)朦肘,不如使用之前創(chuàng)建的 Profile 模型添加字段。Django還允許使用自定義用戶模型双饥,如果想要使用自定義用戶模型媒抠,參考文檔https://docs.djangoproject.com/en/1.11/topics/auth/customizing/#specifying-a-custom-user-model。
向模型本身定義 ManyToManyField 時(shí)我們?cè)O(shè)置了symmetric=False 咏花,Django 將強(qiáng)制關(guān)系對(duì)稱趴生,在這種情況下,我們?cè)O(shè)置 symmetric=False 來(lái)定義非對(duì)稱關(guān)系。也就是說(shuō)冲秽,我關(guān)注你并不意味著你自動(dòng)關(guān)注我舍咖。
注意:
使用中間模型創(chuàng)建多對(duì)多關(guān)系時(shí) 矩父,add() 锉桑、create() 或 remove() 等相關(guān)管理方法失效。對(duì)中間模型進(jìn)行操作需要?jiǎng)?chuàng)建或者刪除中間模型實(shí)例窍株。
運(yùn)行以下命令生成 account 應(yīng)用的遷移文件:
python manage.py makemigrations account
你將看到下面的輸出:
Migrations for 'account':
account/migrations/0002_contact.py
- Create model Contact
現(xiàn)在民轴,運(yùn)行以下命令同步數(shù)據(jù)庫(kù):
python manage.py migrate account
你將看到下面的輸出:
Applying account.0002_contact... OK
Contact 模型現(xiàn)在已經(jīng)同步到數(shù)據(jù)庫(kù)了,我們可以在用戶之間創(chuàng)建關(guān)系了球订。然而后裸,我們的網(wǎng)站現(xiàn)在還不支持瀏覽用戶或查看用戶資料的功能。下面我們將為 User 模型創(chuàng)建列表和詳情視圖冒滩。
為用戶創(chuàng)建列表和詳情視圖
打開(kāi) account 應(yīng)用的 views.py 文件并添加以下代碼:
from django.shortcuts import get_object_or_404
from django.contrib.auth import get_user_model
from django.conf import settings
@login_required
def user_list(request):
users = get_user_model().objects.filter(is_active=True, is_superuser=False)
return render(request, 'account/user/list.html',
{'section': 'people', 'users': users})
@login_required
def user_detail(request, username):
user = get_object_or_404(settings.AUTH_USER_MODEL, username=username,
is_active=True)
return render(request, 'account/user/detail.html',
{'section': 'people', 'user': user})
筆者注:
為了與第四章的內(nèi)容保持一致微驶,這里分別使用 get_user_model 和 settings.AUTH_USER_MODEL 代替了 User。
這是 User 對(duì)象的簡(jiǎn)單列表和詳情視圖开睡。 user_list 視圖獲取所有活躍用戶因苹。Django User 模型包含 is_active 字段,該字段用來(lái)判斷用戶是否處于活躍狀態(tài)篇恒。我們通過(guò) is_active = True
返回處于活躍狀態(tài)的用戶扶檐。這個(gè)視圖將返回所有的結(jié)果,我們可以使用 image_list 視圖的辦法對(duì)結(jié)果進(jìn)行分頁(yè)處理胁艰。
筆者注:
定義user_list 的 user 時(shí)款筑,我們通過(guò)增加設(shè)置
is_superuser=False
返回處于活躍狀態(tài)且不是超級(jí)用戶的用戶。
user_detail 視圖使用 get_object_or_404() 快捷函數(shù)來(lái)獲得給定用戶名的活躍用戶腾么。如果沒(méi)有找到符合給定用戶名的活躍用戶奈梳,視圖將返回 HTTP 404 。
編輯 account 應(yīng)用的 urls.py 文件為每個(gè)視圖添加 URL模式:
url(r'^users/$', views.user_list, name='user_list'),
url(r'^users/(?P<username>[-\w]+)/$', views.user_detail,
name='user_detail'),
我們將使用 user_detail URL模式為用戶生成標(biāo)準(zhǔn)鏈接解虱。我們?cè)谀P椭卸x了一個(gè) get_absolute_url() 方法為每個(gè)對(duì)象返回標(biāo)準(zhǔn)鏈接攘须。另一種指定模型 URL 的方法是在項(xiàng)目 settings.py 中設(shè)置 ABSOLUTE_URL_OVERRIDES 。
編輯項(xiàng)目的 settings.py 文件并添加以下代碼:
ABSOLUTE_URL_OVERRIDES = {
'auth.user': lambda u: reverse_lazy('account:user_detail', args=[u.username])}
django 為 ABSOLUTE_URL_OVERRIDES 設(shè)置的所有模型動(dòng)態(tài)添加 get_absolute_url() 方法饭寺。這個(gè)方法為設(shè)置中的指定模型返回相應(yīng)的 URL 阻课。我們?yōu)樘囟ㄓ脩舴祷?user_detail URL。現(xiàn)在可以通過(guò) User 實(shí)例的 get_absolute_url() 獲取對(duì)應(yīng)的 URL 艰匙。使用 python manage.py shell 打開(kāi) Python shell 并運(yùn)行以下代碼進(jìn)行測(cè)試:
In [1]: from django.contrib.auth.models import User
In [2]: user = User.objects.latest('id')
In [3]: str(user.get_absolute_url())
Out[3]: '/account/users/admin/'
輸出返回了我們期待的 URL限煞。然后需要為剛剛創(chuàng)建的視圖添加模板,在 account 應(yīng)用的 templates/account/ 目錄下添加以下路徑:
編輯 account/user/list.html 模板并添加以下代碼:
{% 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 %}
<img src="{{ im.url }}">
{% endthumbnail %}
</a>
<div class="info">
<a href="{{ user.get_absolute_url }}" class="title">
{{ user.username }}
</a>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
這個(gè)模板可以列出網(wǎng)站上所有處于活躍狀態(tài)的人员凝,對(duì)給定的用戶進(jìn)行迭代并使用 sorl-thumbnail 的 (% thumbnail %} 模板標(biāo)簽生成用戶頭像的縮略圖署驻。
打開(kāi)項(xiàng)目的基礎(chǔ)模板 base.html 并為目錄 People 的 href 屬性添加 user_list 鏈接:
<li {% ifequal section "people" %}class="selected"{% endifequal %}>
<a href="{% url 'account:user_list' %}">People</a>
</li>
使用 python manage.py runserver 命令啟動(dòng)開(kāi)發(fā)服務(wù)器,并在瀏覽器中打開(kāi) http://127.0.0.1:8000/account/users/,我們將看到下面的用戶列表:
筆者注:
顯示上述頁(yè)面旺上,我們需要為 Users 模型添加上面三個(gè)用戶瓶蚂,并且在 Profiles 模型中添加上面三個(gè)用戶并設(shè)置他們的圖片。我們可以使用 admin 網(wǎng)站很快完成上述添加宣吱。
編輯 account 應(yīng)用的 account/user/detail.html 模板并添加以下代碼:
{% extends "base.html" %}
{% load thumbnail %}
{% block title %}{{ user.username }}{% endblock %}
{% block content %}
<h1>{{ user.username }}</h1>
<div class="profile-info">
{% thumbnail user.profile.photo "180x180" crop="100%" as im %}
<img src="{{ im.url }}" class="user-detail">
{% 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="follow button">
{% 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_created.all %}
</div>
{% endwith %}
{% endblock %}
筆者注:
這里將文中 {% block title %} 和 <h1> 元素中user.get_full_name改為了user.username窃这。
user.get_full_name 會(huì)組合user的first_name字段和last_name字段獲得用戶名,而在設(shè)置時(shí)未填寫(xiě)這兩個(gè)字段征候,因此進(jìn)行了修改杭攻。
在上面模板中,我們展示了用戶資料并使用 {% thumbnail %} 模板標(biāo)簽來(lái)展示資料圖片疤坝≌捉猓可以顯示關(guān)注的人數(shù)和關(guān)注/取消關(guān)注的鏈接。如果用戶查看自己的資料則隱藏該鏈接以防止用戶關(guān)注自己跑揉。我們將使用一個(gè) AJAX 請(qǐng)求來(lái)關(guān)注/取消關(guān)注特定用戶锅睛。為 HTML 的 <a> 元素添加 data-id 和 data-action 屬性;來(lái)保存用戶 ID 和點(diǎn)擊時(shí)的初始屬性历谍、關(guān)注或取消關(guān)注现拒,這些屬性取決于請(qǐng)求頁(yè)面的用戶是否已被該用戶關(guān)注。我們通過(guò) list_ajax.html 模板展示用戶標(biāo)記的圖片扮饶。
再次打開(kāi)瀏覽器并點(diǎn)擊一個(gè)已經(jīng)標(biāo)記一些圖片的用戶具练。你將看到下面這樣的資料詳情。
創(chuàng)建 AJAX 視圖來(lái)關(guān)注用戶動(dòng)態(tài)
我們將使用 AJAX 創(chuàng)建一個(gè)簡(jiǎn)單的視圖來(lái)實(shí)現(xiàn)關(guān)注/取消關(guān)注用戶甜无。編輯 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
from django.db import models
@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 = get_user_model().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 models.ObjectDoesNotExist:
return JsonResponse({'status': 'ko'})
return JsonResponse({'status': 'ko'})
我們創(chuàng)建的 user_follow 視圖與之前創(chuàng)建的 image_like 視圖類似扛点。由于我們使用了表示多對(duì)多關(guān)系的自定義中間模型,因此不能再使用 ManyToManyField 管理器的默認(rèn) add() 和 remove() 方法岂丘。所以陵究,我們使用中間模型 Contact 模型來(lái)創(chuàng)建或刪除用戶關(guān)系。
在 account 應(yīng)用的 urls.py 文件中添加以下 URL模式:
url(r'^users/follow/$', views.user_follow, name='user_follow')
這個(gè) URL 要放在 user_detail URL之前奥帘。否則铜邮,任何 /user/follow/ 請(qǐng)求都將匹配 user_detail 模式然后執(zhí)行。 Django 處理任何請(qǐng)求時(shí)都將檢查所有 URL模式直到遇到第一個(gè)匹配的模式然后停止寨蹋。
編輯 account 應(yīng)用的 user/detail.html 模板并在最后添加以下代碼:
{% block domready %}
$('a.follow').click(function(e){
e.preventDefault();
$.post('{% url "accout: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 %}
筆者注:
原文的
$.post('{% url "user_follow" %}'
運(yùn)行時(shí)報(bào)url解析錯(cuò)誤松蒜,這里改為了$.post('{% url "accout:user_follow" %}'
。
這是實(shí)現(xiàn)關(guān)注/取消關(guān)注特定用戶并切換關(guān)注/取消關(guān)注的 AJAX請(qǐng)求的 JavaScript 代碼已旧。我們使用 jQuery 實(shí)現(xiàn) AJAX 請(qǐng)求并基于以前的值設(shè)置 HTML <a> 元素的 data-action 屬性和文本秸苗。實(shí)現(xiàn) AJAX 動(dòng)作時(shí),我們還會(huì)更新頁(yè)面展示的所有關(guān)注用戶的數(shù)量运褪。打開(kāi)一個(gè)已經(jīng)存在的用戶的詳情頁(yè)面并點(diǎn)擊 Follow 鏈接來(lái)測(cè)試我們剛剛實(shí)現(xiàn)的功能惊楼。
創(chuàng)建通用活動(dòng)流應(yīng)用
許多社交網(wǎng)站為用戶提供活動(dòng)流玖瘸,以便用戶查看其他用戶的新活動(dòng)√戳活動(dòng)流表示一個(gè)用戶或者一組用戶最近的活動(dòng)列表雅倒。比如,F(xiàn)acebook 的 News Feed 就是一個(gè)活動(dòng)流弧可∶锵唬活動(dòng)可以包括用戶 X 標(biāo)記了圖片 Y 或者用戶 X 關(guān)注了用戶 Y。我們將創(chuàng)建活動(dòng)流應(yīng)用來(lái)幫助用戶看到他所關(guān)注的用戶最近的活動(dòng)侣诺。為了實(shí)現(xiàn)這項(xiàng)功能殖演,我們將需要一個(gè)模型來(lái)保存用戶在網(wǎng)站上的活動(dòng),以及向 feed 添加活動(dòng)年鸳。
使用以下命令在項(xiàng)目中添加一個(gè)名為 actions 的新應(yīng)用:
python manage.py startapp actions
在項(xiàng)目的 settings.py 的 INSTALLED_APPS 添加 actions 來(lái)激活一個(gè)新應(yīng)用:
INSTALLED_APPS = [...,'actions']
編輯 actions 應(yīng)用的 models.py 文件并添加以下代碼:
from django.conf import settings
from django.db import models
# Create your models here.
class Action(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, 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',)
這是保存用戶活動(dòng)的 Action 模型,該模型的字段包括:
user:活動(dòng)的用戶丸相。它是 User 模型的外鍵搔确;
verb:用戶的活動(dòng)。
created: 創(chuàng)建活動(dòng)的日期和時(shí)間灭忠。這里使用 auto_now_add=True 將該字段的值自動(dòng)設(shè)置為實(shí)例的第一次保存時(shí)間膳算。
這個(gè)基本的模型只能保存用戶 X 做了什么的活動(dòng)。我們還需要一個(gè) ForeignKey 來(lái)保存 target 對(duì)象弛作,比如用戶 X 標(biāo)記了圖片 Y 或者用戶 X 關(guān)注了用戶 Y 涕蜂。我們已經(jīng)知道,一個(gè) ForeignKey 只能指向一個(gè)其它模型映琳。因此机隙,我們需要一種方法來(lái)實(shí)現(xiàn) target 對(duì)象可以指向任何存在的模型的實(shí)例。Django contenttypes 框架可以實(shí)現(xiàn)該功能萨西。
使用 contenttypes 框架
Django 的 contenttypes 框架位于 django.contirb.contenttypes 有鹿。這個(gè)應(yīng)用可以追蹤項(xiàng)目安裝的所有模型并提供通用接口來(lái)實(shí)現(xiàn)模型交互。
我們使用 startproject 命令創(chuàng)建新項(xiàng)目時(shí)谎脯, INSTALLED_APPS 默認(rèn)包含 django.contirb.contenttypes 葱跋。其它 contrib 模塊(如權(quán)限框架、admin應(yīng)用)會(huì)用到它源梭。
contenttypes 應(yīng)用包含一個(gè) ContentType 模型娱俺。這個(gè)模型的實(shí)例表示應(yīng)用的一個(gè)模型。新模型添加到項(xiàng)目時(shí)將會(huì)自動(dòng)創(chuàng)建 ContentType 實(shí)例废麻。ContentType 將有以下字段:
- app_label :模型所屬的應(yīng)用名稱荠卷。它將自動(dòng)獲取模型 Meta 選項(xiàng) app_label 屬性的值。例如脑溢,Image模型屬于 images 應(yīng)用僵朗。
- model:模型類的名稱赖欣。
- name:模型的名稱。自動(dòng)使用 Meta 選項(xiàng)的 verbose_name 屬性的值验庙。
我們來(lái)看下如何與 ContentType 對(duì)象交互顶吮。使用 python manage.py shell 命令打開(kāi) Python 終端。我們通過(guò)設(shè)置 query 的 app_label 和 model 屬性來(lái)獲得指定模型的 ContentType 對(duì)象粪薛,例如:
In [2]: from django.contrib.contenttypes.models import ContentType
In [3]: image_type = ContentType.objects.get(app_label='images',model='image')
In [4]: image_type
Out[4]: <ContentType: image>
模型類可以通過(guò)調(diào)用 model_class() 方法從 ContentType 對(duì)象獲得:
In [5]: image_type.model_class()
Out[5]: images.models.Image
還可以獲得指定模型類的 ContentType 對(duì)象:
In [6]: from images.models import Image
In [7]: ContentType.objects.get_for_model(Image)
Out[7]: <ContentType: image>
這里只是 contenttypes 的部分列子悴了。Django 為其提供很多方法∥ナ伲可以通過(guò)以下網(wǎng)頁(yè)找到 contenttypes 的官方文檔:https://docs.djangoproject.com/en/1.11/ref/contrib/contenttypes/湃交。
為模型添加通用關(guān)系
ContentType 對(duì)象在通用關(guān)系中指向關(guān)系使用的模型。設(shè)置通用關(guān)系需要在模型中設(shè)置三個(gè)字段:
- 指向 ContentType 的 ForeignKey 藤巢。它將告訴我們關(guān)系使用的模型搞莺。
- 保存相關(guān)對(duì)象主鍵的字段。 我們通常使用 PositiveIntegerField 來(lái)匹配 Django 的自動(dòng)主鍵字段掂咒。
- 上面兩個(gè)字段構(gòu)成的通用關(guān)系的定義和管理字段才沧。contenttypes 框架提供一個(gè) GenericForeignKey 字段來(lái)實(shí)現(xiàn)該功能。
編輯 actions 應(yīng)用的 models.py 文件:
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
# Create your models here.
class Action(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, 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:指向 ContentType 模型的 ForeignKey绍刮。
- target_id: 保存相關(guān)對(duì)象主鍵的 PositiveIntegerField温圆。
- target: 基于上面兩個(gè)字段組合的相關(guān)對(duì)象。
Django 不會(huì)在數(shù)據(jù)庫(kù)中創(chuàng)建 GenericForeignKey 字段孩革。能夠映射到數(shù)據(jù)庫(kù)的字段只有 target_ct 和 target_id岁歉。兩個(gè)字段都設(shè)置了 blank=True 和 null=True 以保證在沒(méi)有設(shè)置這兩個(gè)字段的情況下可以保存 Action 對(duì)象。
注意:
在可以使用通用關(guān)系的情況下膝蜈,使用通用關(guān)系比使用外鍵可以使應(yīng)用更加靈活锅移。
運(yùn)行以下命令來(lái)為應(yīng)用創(chuàng)建初始遷移文件:
python manage.py makemigrations actions
可以看到下面的輸出:
Migrations for 'actions':
actions/migrations/0001_initial.py
- Create model Action
然后,運(yùn)行下面的命令來(lái)同步到數(shù)據(jù)庫(kù):
python manage.py migrate
下面的輸出表示完成了同步:
Operations to perform:
Apply all migrations: account, actions, admin, auth, contenttypes, images, sessions, social_django, thumbnail
Running migrations:
Applying actions.0001_initial... OK
我們向 admin 網(wǎng)站添加 Action 模型彬檀。編輯 actions 應(yīng)用的 admin.py 文件添加以下代碼:
from django.contrib import admin
from .models import Action
# Register your models here.
class ActionAdmin(admin.ModelAdmin):
list_display = ('user', 'verb', 'target', 'created')
list_filter = ('created',)
search_fields = ('verb',)
admin.site.register(Action, ActionAdmin)
我們剛剛在admin 網(wǎng)站注冊(cè)了 Action 模型帆啃。運(yùn)行python mange.py runserver 啟動(dòng)開(kāi)發(fā)服務(wù)器并在瀏覽器中打開(kāi)http://127.0.0.1:8000/admin/actions/action/add。你將看到創(chuàng)建新的 Action 對(duì)象的頁(yè)面:
我們可以看到窍帝,這里只顯示了映射到數(shù)據(jù)庫(kù)的 target_ct 和 target_id 字段努潘,但是沒(méi)有顯示 GenericForeignKey 字段。 target_ct 可以是項(xiàng)目中注冊(cè)的任何模型坤学,可以通過(guò)向 ForeignKey 添加 limit_choices_to 屬性來(lái)限制可以選擇的模型:limit_choices_to 幫助我們將 ForeignKey 字段的內(nèi)容限定為一組特定的值疯坤。
在 actions 應(yīng)用目錄下新建名為 utils.py 的文件。我們將定義新建 Action 對(duì)象的快捷函數(shù)深浮,添加以下代碼:
from .models import Action
def create_action(user, verb, target=None):
action = Action(user=user, verb=verb, target=target)
action.save()
create_action() 幫助我們創(chuàng)建包含或者不包含 target 對(duì)象的 action压怠。我們可以在代碼的任何位置使用這個(gè)函數(shù)來(lái)向活動(dòng)流添加新的活動(dòng)。
避免活動(dòng)流中的重復(fù)動(dòng)作
有時(shí)候可能會(huì)多次重復(fù)某些動(dòng)作飞苇。用戶可能在很短時(shí)間內(nèi)多次點(diǎn)擊 like/ unlike 按鈕或者進(jìn)行相同的操作菌瘫。這可能導(dǎo)致保存和展示重復(fù)動(dòng)作蜗顽。為了避免這種情況,我們需要改進(jìn) create_action() 函數(shù)來(lái)避免大多數(shù)重復(fù)動(dòng)作雨让。
編輯 accounts 應(yīng)用的 utils.py 文件并更改為:
import datetime
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
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:
action = Action(user=user, verb=verb, target=target)
action.save()
return True
return False
筆者注:
similar_actions = Action.objects.filter(user_id=user.id, verb=verb, created__gte=last_minute)
原文中為:
similar_actions = Action.objects.filter(user_id=user.id, verb=verb, timestamp__gte=last_minute)
由于 Action 模型中沒(méi)有 timestamp 雇盖,所以進(jìn)行了更改。
我們更改了create_action() 函數(shù)來(lái)避免重復(fù)動(dòng)作并返回動(dòng)作是否保存的布爾值栖忠。我們是這樣避免重復(fù)動(dòng)作的:
首先崔挖,使用 Django 提供的 timezone.now() 獲得當(dāng)前時(shí)間,這個(gè)方法與 datetime.datetime.now() 并不完全相同庵寞,它返回一個(gè) timezone-aware 對(duì)象狸相。Django 提供 USE_TZ 設(shè)置來(lái)使用/禁用時(shí)區(qū)。startproject 命令生成的 settings.py 文件默認(rèn)設(shè)置 USE_TZ = True捐川。
使用 last_minute 變量保存前面一分鐘的時(shí)刻并獲取用戶前面一分鐘至今的所有動(dòng)作脓鹃。
如果前面一分鐘沒(méi)有任何動(dòng)作則創(chuàng)建動(dòng)作。如果創(chuàng)建了 Action 對(duì)象則返回 True 属拾,否則返回 False 将谊。
向活動(dòng)流添加用戶動(dòng)態(tài)
現(xiàn)在要向視圖添加一些動(dòng)作來(lái)創(chuàng)建活動(dòng)流。我們將保存以下類型的交互動(dòng)作:
用戶標(biāo)記一張圖片渐白;
用戶喜歡/取消喜歡 一張圖片;
用戶創(chuàng)建一個(gè)賬戶逞频;
用戶關(guān)注/取消關(guān)注另一個(gè)用戶纯衍;
編輯 image 應(yīng)用的 的 views.py 文件并添加以下內(nèi)容:
from actions.utils import create_action
在 image_create 視圖中的保存圖像之后添加 create_action() :
new_item.save()
create_action(request.user, 'bookmarked image', new_item)
在 image_like 視圖中,處理完用戶喜歡/取消喜歡操作后添加 create_action() 動(dòng)作:
if action == 'like':
image.users_like.add(request.user)
else:
image.users_like.remove(request.user)
create_action(request.user, action, image)
筆者注:
原文在 image.users_like.add(request.user) 后添加的 create_action(request.user, 'like', image)苗胀,這里則無(wú)論用戶是否喜歡都進(jìn)行了記錄襟诸。
現(xiàn)在,編輯 account 應(yīng)用的 views.py 視圖并添加以下內(nèi)容:
from actions.utils import create_action
在 register 視圖中的 Profile 對(duì)象之后添加 create_action:
new_user.save()
profile = Profile.objects.create(user=new_user)
create_action(new_user,'has created an account')
在 user_follow 視圖中添加 create_action:
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()
create_action(request.user,action,user)
我們可以看到基协,通過(guò) Action 模型的 create_action 函數(shù)歌亲,很容易為活動(dòng)流保存新的活動(dòng)。
展示活動(dòng)流
最后澜驮,我們需要向每位用戶展示活動(dòng)流陷揪,我們將在用戶公告板中展示。編輯 account 應(yīng)用的 views.py 文件杂穷,導(dǎo)入 Action 模型并更改 dashboard 視圖:
from actions.models import Action
@login_required
def dashboard(request):
# Diasplay 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.select_related('user', 'user__profile').prefetch_related('target')
actions = actions[:10]
return render(request, 'account/dashboard.html',
{'section': 'dashboard', 'actions': actions})
在這個(gè)視圖中悍缠,我們從數(shù)據(jù)庫(kù)獲取除了當(dāng)前用戶動(dòng)作的所有動(dòng)作。如果用戶沒(méi)有關(guān)注任何人耐量,我們展示其他用戶的最近活動(dòng)飞蚓。如果用戶關(guān)注了其他用戶,我們將活動(dòng)的用戶限制到該用戶關(guān)注的用戶廊蜒。最后我們將返回的活動(dòng)限制為 10 條趴拧。我們這里不使用 order_by() 是由于使用基于 Action 模型的 Meta 選項(xiàng)設(shè)置的 ordering 溅漾。由于我們?cè)?Action 模型中設(shè)置了 ordering = ('-created',),我們首先獲得的是最近的活動(dòng)著榴。
優(yōu)化相關(guān)對(duì)象的QuerySets
每次獲取 Action 對(duì)象添履,我們都可能訪問(wèn)相關(guān)的 User 對(duì)象,甚至還會(huì)訪問(wèn)相關(guān)的 Profile 對(duì)象兄渺。 Django ORM 提供一個(gè)簡(jiǎn)單的方法一次性獲得相關(guān)對(duì)象缝龄,以避免對(duì)數(shù)據(jù)庫(kù)的額外訪問(wèn)。
使用 select_related
Django 提供一個(gè)名為 select_related() 的 query 方法來(lái)實(shí)現(xiàn)獲取一對(duì)多關(guān)系的相關(guān)對(duì)象挂谍。這將轉(zhuǎn)化為一個(gè)更加復(fù)雜的 QuerySet 叔壤,但是可以避免訪問(wèn)相關(guān)對(duì)象時(shí)的額外查詢。select_related 方法也可用于 ForeignKey 和 OneToOne 字段口叙。它通過(guò) SQL 的 JOIN 實(shí)現(xiàn)并且在 Select 語(yǔ)句中的相關(guān)對(duì)象炼绘。
編輯前面的代碼來(lái)使用 select_related():
actions = actions.filter(user_id__in=following_ids)
添加你要使用的字段:
actions = actions.filter(user_id_in=following_ids).select_related(
'user', 'user__profile')
我們使用 user__profile
在一個(gè)SQL 查詢中連接 profile 表。如果在不傳入任何參數(shù)的情況下調(diào)用 select_related
妄田,它將獲得所有外鍵的對(duì)象俺亮。因此一定要將 select_related 限制到前面訪問(wèn)的關(guān)系中。
注意:
小心使用 select_related 可以大大降低查詢時(shí)間疟呐。
使用 prefetch_related
我們已經(jīng)看到脚曾,select_related() 將幫助我們?cè)谝粚?duì)多關(guān)系中獲得相關(guān)對(duì)象。然而 select_related() 不能用于多對(duì)多或者多對(duì)一關(guān)系( ManyToMany 或反向 ForeignKey 字段)启具。除了 select_related()本讥,Django 還為多對(duì)多或者多對(duì)一關(guān)系提供了 prefetch_related() 方法。 prefetch_related() 方法為每個(gè)關(guān)系實(shí)現(xiàn)單獨(dú)的查詢并使用 Python對(duì)結(jié)果進(jìn)行連接鲁冯。這個(gè)方法也適用于 GenericRelation 和 GenericForeignKey 拷沸。
通過(guò)添加 prefetch_related() 獲得目標(biāo) GenericForeignKey 來(lái)完成查詢:
actions = actions.filter(user_id_in=following_ids).select_related(
'user', 'user__profile').prefetch_related('target')
現(xiàn)在,已經(jīng)對(duì)獲得用戶動(dòng)作及相關(guān)對(duì)象的查詢進(jìn)行了優(yōu)化薯演。
為動(dòng)作添加模板
我們將創(chuàng)建模板來(lái)展示特定 Action 對(duì)象撞芍。在 actions 應(yīng)用下新建一個(gè)名為 templates 的目錄,并添加以下文件結(jié)構(gòu):
編輯 actions/action/detail.html 模板文件并添加以下內(nèi)容:
{% load thumbnail %}
{% with user=action.user profile=action.user.profile %}
<div class="action">
<div class="images">
{% if profile.photo %}
{% thumbnail user.profile.photo "80x80" crop="100%" as im %}
<a href="{{ user.get_absolute_url }}">
<img src="{{ im.url }}" alt="{{ user.get_full_name }}"
class="item-img">
</a>
{% endthumbnail %}
{% endif %}
{% if action.target %}
{% with target=action.target %}
{% if target.image %}
{% thumbnail target.image "80x80" crop="100%" as im %}
<a href="{{ target.get_absolute_url }}">
<img src="{{ im.url }}" class="item-img">
</a>
{% endthumbnail %}
{% endif %}
{% endwith %}
{% endif %}
</div>
<div class="info">
<p>
<span class="date">{{ action.created|timesince }} ago</span>
<br/>
<a href="{{ user.get_absolute_url }}">
{{ user.first_name }}
</a>
{{ action.verb }}
{% if action.target %}
{% with target=action.target %}
<a href="{{ target.get_absolute_url }}">{{ target }}</a>
{% endwith %}
{% endif %}
</p>
</div>
</div>
{% endwith %}
這是展示 Action 對(duì)象的模板跨扮。我們使用 {% with %} 模板標(biāo)簽來(lái)獲得用戶的動(dòng)作和資料序无。然后,如果 Action 對(duì)象有相關(guān)目標(biāo)對(duì)象好港,我們將展示目標(biāo)對(duì)象的 圖片愉镰。最后,如果有活動(dòng)用戶钧汹,我們將展示活動(dòng)用戶的連接以及它們的動(dòng)作丈探、目標(biāo)對(duì)象。
現(xiàn)在拔莱,編輯 account/dashboard.html 模板并在 content 塊的底部添加以下代碼:
<h2>What's happening</h2>
<div id="action-list">
{% for action in actions %}
{% include "actions/action/detail.html" %}
{% endfor %}
</div>
現(xiàn)在碗降,在瀏覽器中打開(kāi)http://127.0.0.1:8000/account/隘竭。使用已存在的用戶登錄并進(jìn)行幾次操作一遍保存到數(shù)據(jù)庫(kù)中。然后讼渊,使用另一個(gè)用戶登錄动看,關(guān)注之前的用戶,查看一下公告板頁(yè)面生成的活動(dòng)流爪幻。它應(yīng)該是這樣的:
我們剛剛為用戶創(chuàng)建了一個(gè)完整的活動(dòng)流菱皆,我們還可以輕松的添加新的用戶動(dòng)作。還可以通過(guò)使用之前為 image_list 視圖實(shí)現(xiàn)的 AJAX 分頁(yè)來(lái)為活動(dòng)流添加有限滾動(dòng)效果挨稿。
使用 signals 進(jìn)行非標(biāo)準(zhǔn)化計(jì)數(shù)
有時(shí)我們需要的數(shù)據(jù)不標(biāo)準(zhǔn)仇轻。非標(biāo)準(zhǔn)化通過(guò)數(shù)據(jù)冗余來(lái)優(yōu)化讀取性能。但是我們只能在必要場(chǎng)合非常小心的進(jìn)行非標(biāo)準(zhǔn)化奶甘。非標(biāo)準(zhǔn)化最大的問(wèn)題在于很難更新不標(biāo)準(zhǔn)的數(shù)據(jù)篷店。
我們將看到一個(gè)通過(guò)非標(biāo)準(zhǔn)化計(jì)數(shù)改善查詢性能的例子。它的缺陷在于我們必須持續(xù)更新冗余數(shù)據(jù)臭家。我們將對(duì)Image 模型的數(shù)據(jù)進(jìn)行非標(biāo)準(zhǔn)化并通過(guò) Django 的 signals 來(lái)維持?jǐn)?shù)據(jù)更新疲陕。
使用 signals
Django 提供 signal 適配器來(lái)實(shí)現(xiàn)一些動(dòng)作發(fā)生時(shí)通知某些 receiver 函數(shù)的功能。signal 適用于每次進(jìn)行每個(gè)動(dòng)作后都要進(jìn)行某些其他動(dòng)作的情況 钉赁。你可以創(chuàng)建自己的 signal 以便于一個(gè)事件發(fā)生后其它事件可以得到通知蹄殃。
Django 在 django.db.models.signals 中為模型提供幾個(gè) signal。其中的一些 signal 包括:
- pre_save 和 post_save:調(diào)用模型 save() 方法之前或之后發(fā)送你踩;
- pre_delete 和 post_delete:調(diào)用模型或queryset的 delete() 方法之前或之后發(fā)送窃爷;
- m2m_changed: 模型的ManyToManyField 改變時(shí)發(fā)送;
這些只是 django 提供的 signal 的一部分姓蜂,django 的內(nèi)置 signal 列表見(jiàn)https://docs.djangoproject.com/en/1.11/ref/signals/。
如果想通過(guò)受歡迎程度來(lái)檢索圖片医吊。我們可以使用 Django 集合函數(shù)獲得喜歡圖片的人數(shù)來(lái)進(jìn)行排序钱慢。我們?cè)诘谌乱呀?jīng)使用過(guò)聚合函數(shù)。下面的代碼將通過(guò)喜歡的數(shù)量獲得圖像:
from django.db.models import Count
from images.models import Image
image_by_popularity = Image.objects.annotate(total_likes=Count('users_like')).order_by('-total_likes')
然而卿堂,通過(guò)對(duì)所有的 likes 計(jì)數(shù)進(jìn)行排序比通過(guò)保存總數(shù)的字段進(jìn)行排序浪費(fèi)更多的資源束莫。 我們可以為 Image 模型添加一個(gè)字段來(lái)對(duì) likes 進(jìn)行非標(biāo)準(zhǔn)化計(jì)數(shù)來(lái)增強(qiáng)查詢?cè)撟侄蔚男阅堋D敲磻?yīng)該何更新該字段呢草描?
編輯images 應(yīng)用的models.py 文件览绿,為 Image模型添加以下字段:
total_likes = models.PositiveIntegerField(db_index=True,default=0)
total_likes 字段將幫助我們存儲(chǔ)喜歡每幅圖片的總?cè)藬?shù)。如果需要根據(jù)該字段進(jìn)行排序或者過(guò)濾穗慕,那么非標(biāo)準(zhǔn)化數(shù)據(jù)非常有用饿敲。
注意:
在設(shè)置非標(biāo)準(zhǔn)化字段之前可以考慮其它幾種方法改善性能:設(shè)置數(shù)據(jù)索引、查詢優(yōu)化逛绵、緩存等怀各。
運(yùn)行以下命令來(lái)創(chuàng)建新增加字段的遷移:
python manage.py makemigrations images
你應(yīng)該可以看到下面的輸出:
Migrations for 'images':
images/migrations/0002_image_total_likes.py
- Add field total_likes to image
然后運(yùn)行以下命令實(shí)現(xiàn)遷移:
python manage.py migrate images
輸出應(yīng)該包含以下內(nèi)容:
Operations to perform:
Apply all migrations: account, actions, admin, auth, contenttypes, images, sessions, social_django, thumbnail
Running migrations:
Applying images.0002_image_total_likes... OK
我們將為m2m_changed signal添加一個(gè) receiver 函數(shù)倔韭。在images 應(yīng)用目錄下增加如下圖所示的結(jié)構(gòu)。
筆者注:
signals 官方用戶手冊(cè)中建議為了最小化導(dǎo)入代碼的副作用瓢对,建議不要將 signal 放在應(yīng)用的根目錄或者 models 模塊中寿酌,因此這里根據(jù)建議增加了 signals 子模塊。
具體參考:https://docs.djangoproject.com/en/1.11/topics/signals/#defining-and-sending-signals
向 handlers.py 中添加以下代碼:
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from images.models import Image
@receiver(m2m_changed, sender=Image.users_like.through)
def user_like_changed(sender, instance, **kwargs):
instance.total_likes = instance.users_like.count()
instance.save()
首先硕蛹,我們使用 receiver() 裝飾器將 users_like_changed 函數(shù)注冊(cè)為 receiver 函數(shù)醇疼。我們將函數(shù)連接到 Image.users_like.through 以便于只有發(fā)送 m2m_changed signal 時(shí)才調(diào)用該函數(shù)。我們還可以使用另外一種方法來(lái)注冊(cè) receiver 函數(shù)法焰,該方法與 使用 Signal 對(duì)象的 connect() 方法一致秧荆。
筆者注:
users_like_changed 的輸入:
sender
Image.users_like.through
(內(nèi)部多對(duì)多關(guān)系模型類)instance
要修改的 Image
實(shí)例
注意:
django signals 是同步阻塞的。不要將signals 與 異步任務(wù)混為一談壶栋。然而辰如,當(dāng)你的代碼收到signal通知后可以結(jié)合使用異步任務(wù)。
你可以將一個(gè)接收函數(shù)連接到 signal 贵试,這樣可以在每次發(fā)送 signal 之后調(diào)用該函數(shù)琉兜。注冊(cè) signal 的推薦方法為在你應(yīng)用配置類的 ready() 方法中進(jìn)行導(dǎo)入。Django 提供了一個(gè)應(yīng)用程序注冊(cè)表毙玻,允許您配置和檢查應(yīng)用程序豌蟋。
定義應(yīng)用配置類
Django 允許用戶指定應(yīng)用配置類。創(chuàng)建一個(gè)繼承 django.apps 的 AppConfig 類的自定義類來(lái)為應(yīng)用提供自定義配置桑滩。應(yīng)用配置類允許用戶存儲(chǔ) metadata 和應(yīng)用配置以及實(shí)現(xiàn)檢查功能梧疲。
關(guān)于應(yīng)用配置的更多信息見(jiàn)https://docs.djangoproject.com/en/1.11/ref/applications/。
注冊(cè) signal receiver 函數(shù)(使用 receiver() 裝飾器)运准,只需在 AppConfig 類的 ready() 方法中導(dǎo)入 應(yīng)用的 signal模塊幌氮。應(yīng)用注冊(cè)時(shí)會(huì)調(diào)用 ready() 方法。應(yīng)用的其他初始化也應(yīng)該放在這個(gè)方法中胁澳。
在images 應(yīng)用目錄下的apps.py 文件中將源代碼更改為:
from django.apps import AppConfig
class ImagesConfig(AppConfig):
name = 'images'
verbose_name = 'Image bookmarks'
def ready(self):
# import signal handler
from .signals import handlers
筆者注:
原文需要新建 apps.py 文件该互,在 Django1.11 版本中項(xiàng)目初始化會(huì)自動(dòng)生成 apps.py 文件,只需對(duì)其進(jìn)行更改即可韭畸。
name 屬性定義了應(yīng)用的 Python 路徑宇智,verbose_name 為應(yīng)用設(shè)置了人性化名稱,它將會(huì)顯示在 admin 網(wǎng)站中胰丁。ready() 方法為應(yīng)用導(dǎo)入 signals随橘。
現(xiàn)在,我們需要告訴 django 使用的應(yīng)用配置類锦庸,編輯 images 應(yīng)用目錄的__init__
函數(shù)并添加以下內(nèi)容:
default_app_config = 'images.apps.ImagesConfig'
打開(kāi)瀏覽器查看圖像細(xì)節(jié)頁(yè)面并點(diǎn)擊like按鈕机蔗。回到admin網(wǎng)站并看一下total_likes 屬性。你應(yīng)該可以看到total_likes 屬性進(jìn)行了更新:
現(xiàn)在蜒车,我們可以使用 total_likes 屬性實(shí)現(xiàn)按照受歡迎程度對(duì)圖片進(jìn)行排序并在任何位置進(jìn)行展示讳嘱,這樣可以避免復(fù)雜查詢。下面的通過(guò)對(duì) like 數(shù)量排序進(jìn)行圖片查詢:
images_by_popularity = Image.objects.annotate(likes=Count('users_like')).order_by('-like')
可以變?yōu)椋?/p>
images_by_popularity = Image.objects..order_by('-total_likes')
這將減少 SQL 查詢消耗酿愧。這只是使用 django signal 的一個(gè)例子沥潭。
注意:
由于很難分析控制流,因此要小心使用 signals 嬉挡。如果通知哪個(gè) receiver 钝鸽,盡量避免使用 signal 。
我們需要設(shè)置初始計(jì)數(shù)來(lái)匹配數(shù)據(jù)庫(kù)的當(dāng)前狀態(tài)庞钢。使用 python manage.py shell 命令打開(kāi) shell 并運(yùn)行以下代碼:
from images.models import Image
for image in Image.objects.all():
image.total_likes = image.users_like.count()
image.save()
現(xiàn)在拔恰,每幅圖片用戶喜歡數(shù)量已經(jīng)更新了。
使用 Redis 保存視圖
Redis 是一個(gè)高級(jí)鍵值對(duì)數(shù)據(jù)庫(kù)基括,我們可以用它來(lái)保存不同類型的數(shù)據(jù)颜懊, I/O 操作非常快风皿。Redis 的所有內(nèi)容都存儲(chǔ)在內(nèi)存中河爹,但是數(shù)據(jù)可以偶爾通過(guò)數(shù)據(jù)集暫時(shí)轉(zhuǎn)存到磁盤(pán)上或著通過(guò)添加每個(gè)命令到日志。與其他鍵值存儲(chǔ)相比桐款,Redis 功能非常強(qiáng)大:它提供了一組強(qiáng)大的命令集咸这,并支持各種數(shù)據(jù)結(jié)構(gòu),如字符串魔眨、哈希冰垄、列表肪虎、集合、有序集合虽填,甚至可以使用位圖或 HyperLogLogs 虏肾。
SQL 最好用于模式定義的持久數(shù)據(jù)存儲(chǔ)一汽,但是當(dāng)處理快速變化的數(shù)據(jù)聪姿、易變存儲(chǔ)或需要快速緩存時(shí)算芯,Redis 具有很多優(yōu)勢(shì)。我們來(lái)看看如何使用 Redis 為項(xiàng)目實(shí)現(xiàn)新功能侥啤。
安裝 Redis
從 http://redis.io/download 下載最新的 Redis 版本。解壓 tar.gz 文件茬故,進(jìn)入 redis 目錄并使用下面的 make 命令進(jìn)行編譯:
cd redis-4.0.6
make
安裝完成后使用下面的命令來(lái)初始化 Redis server :
src/redis-server
您應(yīng)該看到以下面內(nèi)容結(jié)束的輸出:
4512:M 16 Jan 09:40:05.728 * Increased maximum number of open files to 10032 (it was originally set to 256).
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 4.0.6 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in standalone mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 6379
| `-._ `._ / _.-' | PID: 4512
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
4512:M 16 Jan 09:40:05.730 # Server initialized
4512:M 16 Jan 09:40:05.731 * Ready to accept connections
默認(rèn)情況下盖灸,redis 運(yùn)行在 6379 端口,但是我們也可以使用 —port 指定自定義端口磺芭,比如 redis-server —port 6655赁炎。當(dāng)服務(wù)器就緒后,我們可以使用以下命令在另一個(gè) shell 中打開(kāi) Redis 客戶端:
src/redis-cli
你應(yīng)該可以看到 Redis 客戶端輸出:
127.0.0.1:6379>
Redis客戶端可以用來(lái)執(zhí)行 Redis 命令。我們來(lái)嘗試一些命令徙垫。在Redis shell 中輸入 SET 命令來(lái)將至保存到鍵中:
127.0.0.1:6379> SET name "Peter"
OK
前面的命令在 Redis 數(shù)據(jù)庫(kù)中創(chuàng)建一個(gè)鍵為 name讥裤、值為 “Peter“ 的鍵值對(duì)。輸出的OK表示已經(jīng)成功保存該鍵值對(duì)姻报。然后己英,使用 GET 命令得到值:
127.0.0.1:6379> GET name
"Peter"
也可以使用 EXISTS 命令判斷是否存在某個(gè)鍵,如果存在則返回 1 吴旋,不存在則返回 0 :
127.0.0.1:6379> EXISTS name
(integer) 1
可以使用 EXPIRE 命令為鍵設(shè)置以秒為單位的生存時(shí)間损肛。還可以使用 EXPIREAT 命令設(shè)置 UNIX 時(shí)間戳的生存時(shí)間。Key 過(guò)期對(duì)于 Redis 用于緩存或存儲(chǔ)易失性數(shù)據(jù)非常有用
127.0.0.1:6379> GET name
"Peter"
127.0.0.1:6379> EXPIRE name 2
(integer) 1
等待 2 秒鐘并嘗試獲得再次獲得該鍵:
127.0.0.1:6379> GET name
(nil)
nil 表示空響應(yīng)荣瑟,這意味著沒(méi)有找到對(duì)應(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)
這些只是鍵操作的基本命令。Redis 包含用于字符串笆焰、哈希劫谅、集合、有序集合等數(shù)據(jù)類型的大量命令嚷掠。你可以從 http://redis.io/commands 查看所有 Redis 命令以及從http://redis.io/data-types 查看所有 Redis 數(shù)據(jù)類型捏检。
在 Python 中使用 Redis
我們需要 Python 綁定 Redis 。通過(guò)pip 命令安裝 redis-py :
pip install redis
可以在http://redis-py.readthedocs.org/找到redis-py 文檔叠国。
redis-py 提供兩個(gè)類來(lái)實(shí)現(xiàn) Redis 交互 : StrictRedis 和 Redis未檩。 它們提供相同的功能。StrictRedis 類遵守官方Redis 命令語(yǔ)法粟焊。 Redis 類擴(kuò)展了 StrictRedis 冤狡,覆蓋了一些方法來(lái)實(shí)現(xiàn)向后兼容。 我們將使用遵循 Redis 命令語(yǔ)法的 StrictRedis 類项棠。 打開(kāi) Python shell 并執(zhí)行以下代碼:
>>> import redis
>>> r = redis.StrictRedis(host='localhost',port=6379,db=0)
代碼創(chuàng)建了與 Redis 數(shù)據(jù)庫(kù)的連接悲雳。在 Redis 中,數(shù)據(jù)庫(kù)使用整數(shù)索引識(shí)別香追。默認(rèn)情況下合瓢,客戶端將連接數(shù)據(jù)庫(kù)0⊥傅洌可以獲得數(shù)據(jù)庫(kù)可以設(shè)置到 16 晴楔,可以通過(guò) redis.conf 文件對(duì)其進(jìn)行更改。
現(xiàn)在使用 python shell 設(shè)置一個(gè)鍵:
>>> r.set('foo','bar')
True
命令返回 True 表示設(shè)置成功∏椭洌現(xiàn)在可以使用 get 命令獲得這個(gè)鍵的值:
>>> r.get('foo')
'bar'
我們可以看到税弃,StrictRedis 的方法遵循 Redis 命令語(yǔ)法。
我們來(lái)將 Redis 集成到項(xiàng)目中凑队,編輯 bookmarks 項(xiàng)目的 settings.py 文件并添加下面的內(nèi)容:
# redis config
REDIS_HOST = 'localhost'
REDIS_PORT = 6379
REDIS_DB = 0
這是 Redis 服務(wù)器的設(shè)置则果,現(xiàn)在我們的項(xiàng)目可以使用這個(gè)數(shù)據(jù)庫(kù)了。
在 Redis 中保存視圖
我們來(lái)存儲(chǔ)一副圖片被查看的次數(shù)。如果我們使用 Django ORM西壮,那么每次展示圖片都需要更新一個(gè) SQL遗增。因此,我們只需要更新一個(gè)保存在內(nèi)存中的計(jì)數(shù)器款青,這樣性能可以好很多做修。
編輯 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 連接以便在視圖中使用 redis 。編輯 image_detail 視圖:
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è)視圖中可都,我們使用 INCR 命令來(lái)為某個(gè)鍵的值增加 1 缓待,如果鍵不存在則將其操作之前將其值設(shè)為 0 。incr() 方法返回操作后鍵的值渠牲,我們將其存儲(chǔ)到 total_views 變量中旋炒。這里我們使用 object-type:id:field
(例如:Image:33:id ) 的方法創(chuàng)建 redis 鍵。
注意:
Redis 鍵命名的簡(jiǎn)便方法為使用冒號(hào)分隔來(lái)創(chuàng)建命鍵签杈。通過(guò)這樣做瘫镇,鍵名可以特別詳細(xì),相關(guān)鍵在其名稱中共享相同模式的一部分答姥。
編輯 image/detail.html 模板并在<span class="count">元素后添加以下代碼:
<span class="count">
<span class="total">{{ total_views }}</span>
view{{ total_views|pluralize }}
</span>
現(xiàn)在铣除,在瀏覽器中打開(kāi)圖片詳情頁(yè)面并進(jìn)行幾次加載,我們可以看到每次查看次數(shù)都會(huì)增加1.看下面的例子:
我們已經(jīng)成功的將 Redis 集成到項(xiàng)目中來(lái)進(jìn)行計(jì)數(shù)鹦付。
在 Redis 中保存排序
我們使用 redis 實(shí)現(xiàn)一些其他功能尚粘。我們將創(chuàng)建平臺(tái)上查看最多的圖片的排序。為了實(shí)現(xiàn)排序敲长,我們需要使用Redis 有序集合郎嫁。有序集合是字符串的非重復(fù)集合,其中的任何一個(gè)都有一個(gè)值祈噪,我們按照這個(gè)值進(jìn)行排序泽铛。
編輯images 應(yīng)用的views.py 文件并更改image_detail 視圖:
@login_required
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})
我們使用 zincby() 命令來(lái)在 image_ranking 有序集合中保存圖片視圖,我們保存了圖片 id 辑鲤,并且將有序集合中該元素的值加 1 盔腔。這將幫助我們?nèi)肿粉櫵袌D片視圖并且保存所有視圖的排序集合。
現(xiàn)在月褥,創(chuàng)建一個(gè)新的視圖來(lái)展示圖片排序弛随,在 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, 'image/image/ranking.html',
{'section': 'images', 'most_viewed': most_viewed})
這是 image_ranking 視圖。我們使用 zrange() 命令來(lái)獲得有序集合中的元素宁赤。這個(gè)命令實(shí)現(xiàn)了從低到高的自定義排序撵幽。通過(guò)使用 0 作為最低、1 作為最高命令 Redis 獲得有序集合的所有元素礁击。我們還可以設(shè)置 desc=True 來(lái)進(jìn)行倒序排序。最后,我們使用 [:10] 來(lái)獲得最高分的前十個(gè)元素哆窿。我們建立保存返回圖片 id 的列表并將其保存在image_ranking_ids 中链烈。獲得 ID 對(duì)應(yīng)圖片對(duì)象并通過(guò) list 函數(shù)執(zhí)行查詢。執(zhí)行查詢非常重要挚躯,因?yàn)槲覀兒竺嬉獙?duì)其使用 sort() 列表方法(我們需要對(duì)象列表强衡,而不是 queryset )。通過(guò) id 在排序中的位置對(duì) Image 對(duì)象進(jìn)行排序÷肜螅現(xiàn)在漩勤,我們可以使用模板中的 most_viewed 列表展示查看的最多的圖片了。
新建 image/ranking.html 模板文件并添加以下代碼:
{% 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_obsolute_url }}">{{ image.title }}</a>
</li>
{% endfor %}
</ol>
{% endblock %}
這個(gè)模板非常簡(jiǎn)單缩搅,我們只是對(duì) most_viewed 中的 image 對(duì)象進(jìn)行迭代越败。
最后,為新視圖創(chuàng)建URL模式硼瓣。編輯 image 應(yīng)用的 urls.py 文件并添加以下模式:
url(r'^ranking/$',views.image_ranking,name='create'),
在瀏覽器中打開(kāi)http://127.0.0.1:8000/images/ranking/究飞,應(yīng)該可以看到這樣圖片排序:
使用 Redis 的下一步工作
Redis 不能代替 SQL 數(shù)據(jù)庫(kù),但是某些任務(wù)更適合使用快速的內(nèi)存存儲(chǔ)堂鲤。將其添加到您的工具庫(kù)亿傅,并在真正需要時(shí)使用它。 以下是 Redis 適用的一些情況:
- 計(jì)數(shù):你已經(jīng)看到瘟栖,Redis 可以很方便的管理計(jì)數(shù)葵擎。可以使用 incr() 和 incrby() 來(lái)進(jìn)行計(jì)數(shù)半哟。
- 保存最新的項(xiàng):可以使用 lpush() 和 rpush() 向列表的開(kāi)頭/結(jié)尾添加項(xiàng)酬滤。使用 lpop()/rpop() 刪除或返回第一個(gè)/最后一個(gè)元素。使用 ltrim() 截取長(zhǎng)度來(lái)保持列表長(zhǎng)度镜沽。
- 隊(duì)列:除了推送和彈出命令之外敏晤,Redis 還提供阻塞隊(duì)列命令。
- 緩存:使用 expire() 和 expireat() 來(lái)將 Redis 作為緩存缅茉。 您還可以找到 Django 的第三方 Redis 緩存后端嘴脾。
- 發(fā)布/訂閱:Redis提供訂閱/取消訂閱和向通道發(fā)送消息的命令。
- 排名和排行榜:使用 Redis 有序集合可以非常容易地創(chuàng)建排行榜蔬墩。
- 實(shí)時(shí)跟蹤:Redis 快速 I/O 可以完美用于實(shí)時(shí)場(chǎng)景译打。
總結(jié)
本章,我們創(chuàng)建了關(guān)注系統(tǒng)和用戶活動(dòng)流拇颅,學(xué)習(xí)了 Django signal 機(jī)理并將 Redis 集成到項(xiàng)目中奏司。
下一章,我們將學(xué)習(xí)如何創(chuàng)建在線商店樟插。我們將創(chuàng)建產(chǎn)品目錄韵洋,使用 session 創(chuàng)建購(gòu)物車竿刁,并學(xué)習(xí)如何通過(guò) Celery 實(shí)現(xiàn)異步任務(wù)。