6 跟蹤用戶動(dòng)作
在上一章中,你用jQuery實(shí)現(xiàn)了AJAX視圖,并構(gòu)建了一個(gè)分享其它網(wǎng)站內(nèi)容的JavaScript書簽工具。
本章中,你將學(xué)習(xí)如何構(gòu)建關(guān)注系統(tǒng)和用戶活動(dòng)流。你會(huì)了解Django的信號(hào)(signals)如何工作,并在項(xiàng)目中集成Redis快速I/O存儲(chǔ),用于存儲(chǔ)項(xiàng)視圖。
本章將會(huì)覆蓋以下知識(shí)點(diǎn):
- 用中介模型創(chuàng)建多對多關(guān)系
- 構(gòu)建AJAX視圖
- 創(chuàng)建活動(dòng)流應(yīng)用
- 為模型添加通用關(guān)系
- 優(yōu)化關(guān)聯(lián)對象的
QuerySet
- 使用信號(hào)進(jìn)行反規(guī)范化計(jì)數(shù)
- 在Redis中存儲(chǔ)項(xiàng)的瀏覽次數(shù)
6.1 構(gòu)建關(guān)注系統(tǒng)
我們將在項(xiàng)目中構(gòu)建關(guān)注系統(tǒng)。用戶可以相互關(guān)注,并跟蹤其他用戶在平臺(tái)分享的內(nèi)容。用戶之間是多對多的關(guān)系,一個(gè)用戶可以關(guān)注多個(gè)用戶,也可以被多個(gè)用戶關(guān)注。
6.1.1 用中介模型創(chuàng)建多對多關(guān)系
在上一章中,通過在一個(gè)關(guān)聯(lián)模型中添加ManyToManyField
,我們創(chuàng)建了多對多的關(guān)系,并讓Django為這種關(guān)系創(chuàng)建了一張數(shù)據(jù)庫表。這種方式適用于大部分情況,但有時(shí)候你需要為這種關(guān)系創(chuàng)建一個(gè)中介模型。當(dāng)你希望存儲(chǔ)這種關(guān)系的額外信息(比如關(guān)系創(chuàng)建的時(shí)間,或者描述關(guān)系類型的字段)時(shí),你需要?jiǎng)?chuàng)建中介模型。
我們將創(chuàng)建一個(gè)中介模型用于構(gòu)建用戶之間的關(guān)系。我們使用中介模型有兩個(gè)原因:
- 我們使用的是Django提供的
User
模型,不想修改它。 - 我們想要存儲(chǔ)關(guān)系創(chuàng)建的時(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)
我們將把Contact
模型用于用戶關(guān)系。它包括以下字段:
-
user_from
:指向創(chuàng)建關(guān)系的用戶的ForeignKey
-
user_to
:指向被關(guān)注用戶的ForeignKey
-
created
:帶auto_new_add=True
的DateTimeField
字段,存儲(chǔ)創(chuàng)建關(guān)系的時(shí)間
數(shù)據(jù)庫會(huì)自動(dòng)在ForeignKey
字段上創(chuàng)建索引。我們在created
字段上用db_index=True
創(chuàng)建數(shù)據(jù)庫索引。當(dāng)用這個(gè)字段排序QuerySet
時(shí),可以提高查詢效率。
通過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)
關(guān)聯(lián)管理器rel_from_set
和rel_to_set
會(huì)返回Contact
模型的QuerySet
。為了從User
模型訪問關(guān)系的另一端,我們希望User
模型包括一個(gè)ManyToManyField
,如下所示:
following = models.ManyToManyField(
'self',
through=Contact,
related_name='followers',
symmetrical=False)
這個(gè)例子中,通過在ManyToManyField
字段中添加through=Contact
,我們告訴Django使用自定義的中介模型。這是從User
模型到它自身的多對多關(guān)系:我們在ManyToManyField
字段中引用'self'
來創(chuàng)建到同一個(gè)模型的關(guān)系。
當(dāng)你在多對多的關(guān)系中需要額外字段時(shí),可以在關(guān)系兩端創(chuàng)建帶
ForeignKey
的自定義模型。在其中一個(gè)關(guān)聯(lián)模型中添加ForeignKey
,并通過through
參數(shù)指向中介模型,讓Django使用該中介模型。
如果User
模型屬于我們的應(yīng)用,我們就可以把上面這個(gè)字段添加到模型中。但是我們不能直接修改它,因?yàn)樗鼘儆?code>django.contrib.auth應(yīng)用。我們將采用略微不同的方法:動(dòng)態(tài)的添加該字段到User
模型中。編輯account
應(yīng)用的models.py
文件,添加以下代碼:
User.add_to_class('following',
models.ManyToManyField('self',
through=Contact,
related_name='followers',
symmetrical=False))
在這段代碼中,我們使用Django模型的add_to_class()
方法添加monkey-patch
到User
模型中。不推薦使用add_to_class()
為模型添加字段。但是,我們在這里使用這種方法有以下幾個(gè)原因:
- 通過Django ORM的
user.followers.all()
和user.following.all()
,可以簡化檢索關(guān)聯(lián)對象。我們使用Contact
中介模型,避免涉及數(shù)據(jù)庫連接(join)的復(fù)雜查詢。如果我們在Profile
模型中定義關(guān)系,則需要使用復(fù)雜查詢。 - 這個(gè)多對多關(guān)系的數(shù)據(jù)庫表會(huì)使用
Contact
模型創(chuàng)建。因此,動(dòng)態(tài)添加的ManyToManyField
不會(huì)對Django的User
模型數(shù)據(jù)庫做任何修改。 - 我們避免創(chuàng)建自定義的用戶模型,充分利用Django內(nèi)置的
User
模型。
記住,在大部分情況下都推薦使用添加字段到我們之前創(chuàng)建的Profile
模型,而不是添加monkey-patch
到User
模型。Django也允許你使用自定義的用戶模型。如果你想使用自定義的用戶模型,請參考文檔。
你可以看到,關(guān)系中包括symmetrical=False
。當(dāng)你定義ManyToManyField
到模型自身時(shí),Django強(qiáng)制關(guān)系是對稱的。在這里,我們設(shè)置symmetrical=False
定義一個(gè)非對稱關(guān)系。也就是說,如果我關(guān)注了你,你不會(huì)自動(dòng)關(guān)注我。
當(dāng)使用中介模型定義多對多關(guān)系時(shí),一些關(guān)系管理器的方法將不可用,比如
add()
,create()
,remove()
。你需要?jiǎng)?chuàng)建或刪除中介模型來代替。
執(zhí)行以下命令為account
應(yīng)用生成初始數(shù)據(jù)庫遷移:
python manage.py makemigrations account
你會(huì)看到以下輸出:
Migrations for 'account':
account/migrations/0002_contact.py
- Create model Contact
現(xiàn)在執(zhí)行以下命令同步數(shù)據(jù)庫和應(yīng)用:
python manage.py migrate account
你會(huì)看到包括下面這一行的輸出:
Applying account.0002_contact... OK
現(xiàn)在Contact
模型已經(jīng)同步到數(shù)據(jù)庫中,我們可以在用戶之間創(chuàng)建關(guān)系了。但是我們的網(wǎng)站還不能瀏覽用戶,或者查看某個(gè)用戶的個(gè)人資料。讓我們?yōu)?code>User模型創(chuàng)建列表和詳情視圖。
6.1.2 為用戶資料創(chuàng)建列表和詳情視圖
打開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
對象簡單的列表和詳情視圖。user_list
視圖獲得所有激活的用戶。Django的User
模型包括一個(gè)is_active
標(biāo)記,表示用戶賬戶是否激活。我們通過is_active=True
過濾查詢,只返回激活的用戶。這個(gè)視圖返回了所有結(jié)果,你可以跟image_list
視圖那樣,為它添加分頁。
user_detail
視圖使用get_object_or_404()
快捷方法,檢索指定用戶名的激活用戶。如果沒有找到指定用戶名的激活用戶,該視圖返回HTTP 404響應(yīng)。
編輯account
應(yīng)用的urls.py
文件,為每個(gè)視圖添加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模式為用戶生成標(biāo)準(zhǔn)URL。你已經(jīng)在模型中定義過get_absolute_url()
方法,為每個(gè)對象返回標(biāo)準(zhǔn)URL。另一種方式是在項(xiàng)目中添加ABSOLUTE_URL_OVERRIDES
設(shè)置。
編輯項(xiàng)目的settings.py
文件,添加以下代碼:
ABSOLUTE_URL_OVERRIDES = {
'auth.user': lambda u: reverse_lazy('user_detail', args=[u.username])
}
Django為ABSOLUTE_URL_OVERRIDES
設(shè)置中的所有模型動(dòng)態(tài)添加get_absolute_url()
方法。這個(gè)方法返回給定模型的對應(yīng)URL。我們?yōu)榻o定用戶返回user_detail
URL。現(xiàn)在你可以在User
實(shí)例上調(diào)用get_absolute_url()
方法獲得相應(yīng)的URL。用python manage.py shell
打開Python終端,執(zhí)行以下命令測試:
>>> from django.contrib.auth.models import User
>>> user = User.objects.latest('id')
>>> str(user.get_absolute_url())
'/account/users/Antonio/'
返回的結(jié)果是期望的URL。我們需要為剛創(chuàng)建的視圖創(chuàng)建模板。在account
應(yīng)用的templates/account/
目錄中添加以下目錄和文件:
user/
detail.html
list.html
編輯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 %}

{% endthumbnail %}
</a>
<div class="info">
<a href="{{ user.get_absolute_url }}" class="title">
{{ user.get_full_name }}
</a>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
該模板列出網(wǎng)站中所有激活的用戶。我們迭代給定的用戶,使用sorl-thumbnail
的{% thumbnail %}
模板標(biāo)簽生成個(gè)人資料的圖片縮略圖。
打開項(xiàng)目的base.html
文件,在以下菜單項(xiàng)的href
屬性中包括user_list
URL:
<li {% if section == "people" %}class="selected"{% endif %}>
<a href="{% url "user_list" %}">People</a>
</li>
執(zhí)行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 %}

{% 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 %}
我們在詳情模板中顯示用戶個(gè)人資料,并使用{% thumbnail %}
模板標(biāo)簽顯示個(gè)人資料圖片。我們顯示關(guān)注者總數(shù)和一個(gè)用于follow/unfollow
的鏈接。如果用戶正在查看自己的個(gè)人資料,我們會(huì)隱藏該鏈接,防止用戶關(guān)注自己。我們將執(zhí)行AJAX請求來follow/unfollow
指定用戶。我們在<a>
元素中添加data-id
和data-action
屬性,其中分別包括用戶ID和點(diǎn)擊鏈接時(shí)執(zhí)行的操作(關(guān)注或取消關(guān)注),這取決于請求該頁面的用戶是否已經(jīng)關(guān)注了這個(gè)用戶。我們用list_ajax.html
模板顯示這個(gè)用戶標(biāo)記過的圖片。
再次打開瀏覽器,點(diǎn)擊標(biāo)記過一些圖片的用戶。你會(huì)看到個(gè)人資料詳情,如下圖所示:
6.1.3 構(gòu)建關(guān)注用戶的AJAX視圖
我們將使用AJAX創(chuàng)建一個(gè)簡單視圖,用于關(guān)注或取消關(guān)注用戶。編輯account
用于的views.py
文件,添加以下代碼:
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from common.decrorators 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
視圖跟我們之前創(chuàng)建的image_like
視圖很像。因?yàn)槲覀優(yōu)橛脩舻亩鄬Χ嚓P(guān)系使用了自定義的中介模型,所以ManyToManyField
自動(dòng)生成的管理器的默認(rèn)add()
和remove()
方法不可用了。我們使用Contact
中介模型創(chuàng)建或刪除用戶關(guān)系。
在account
應(yīng)用的urls.py
文件中導(dǎo)入你剛創(chuàng)建的視圖,然后添加以下URL模式:
url(r'^users/follow/$', views.user_follow, name='user_follow'),
確保你把這個(gè)模式放在user_detail
模式之前。否則任何到/users/follow/
的請求都會(huì)匹配user_detail
模式的正則表達(dá)式,然后執(zhí)行user_detail
視圖。記住,每一個(gè)HTTP請求時(shí),Django會(huì)按每個(gè)模式出現(xiàn)的先后順序匹配請求的URL,并在第一次匹配成功后停止。
編輯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í)行關(guān)注或取消關(guān)注指定用戶的AJAX請求,同時(shí)切換follow/unfollow
鏈接。我們用jQuery執(zhí)行AJAX請求,并根據(jù)之前的值設(shè)置data-action
屬性和<a>
元素的文本。AJAX操作執(zhí)行完成后,我們更新頁面顯示的關(guān)注總數(shù)。打開一個(gè)已存在用戶的詳情頁面,點(diǎn)擊FOLLOW
鏈接,測試我們剛添加的功能。
6.2 構(gòu)建通用的活動(dòng)流應(yīng)用
很多社交網(wǎng)站都會(huì)給用戶顯示活動(dòng)流,讓用戶可以跟蹤其他用戶在平臺(tái)上做了什么。活動(dòng)流是一個(gè)或一組用戶最近執(zhí)行的活動(dòng)列表。比如,F(xiàn)acebook的News Feed就是一個(gè)活動(dòng)流。又或者用戶X標(biāo)記了圖片Y,或者用戶X不再關(guān)注用戶Y。我們將構(gòu)造一個(gè)活動(dòng)流應(yīng)用,讓每個(gè)用戶都可以看到他關(guān)注的用戶最近的操作。要實(shí)現(xiàn)這個(gè)功能,我們需要一個(gè)模型,存儲(chǔ)用戶在網(wǎng)站中執(zhí)行的操作,并提供簡單的添加操作的方式。
用以下命令在項(xiàng)目中創(chuàng)建actions
應(yīng)用:
django-admin startapp actions
在項(xiàng)目的settings.py
文件的INSTALLED_APPS
中添加actions
,讓Django知道新應(yīng)用已經(jīng)激活:
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', )
這是Actioin
模型,用于存儲(chǔ)用戶活動(dòng)。該模型的字段有:
-
user
:執(zhí)行這個(gè)操作的用戶。這是一個(gè)指向Django的User
模型的ForeignKey
。 -
verb
:描述用戶執(zhí)行的操作。 -
created
:該操作創(chuàng)建的日期和時(shí)間。我們使用auto_now_add=True
自動(dòng)設(shè)置為對象第一次在數(shù)據(jù)庫中保存的時(shí)間。
通過這個(gè)基礎(chǔ)模型,我們只能存儲(chǔ)類似用戶X做了某些事情的操作。我們需要一個(gè)額外的ForeignKey
字段,存儲(chǔ)涉及目標(biāo)對象的操作,比如用戶X標(biāo)記了圖片Y,或者用戶X關(guān)注了用戶Y。你已經(jīng)知道,一個(gè)普通的ForeignKey
字段只能指向另一個(gè)模型。但是我們需要一種方式,讓操作的目標(biāo)對象可以是任何一個(gè)已經(jīng)存在的模型的實(shí)例。這就是Django的contenttypes
框架的作用。
6.2.1 使用contenttypes框架
Django的contenttypes
框架位于django.contrib.contenttypes
中。這個(gè)應(yīng)用可以跟蹤項(xiàng)目中安裝的所有模型,并提供一個(gè)通用的接口與模型交互。
當(dāng)你使用startproject
命令創(chuàng)建新項(xiàng)目時(shí),django.contrib.contenttypes
已經(jīng)包括在INSTALLED_APPS
設(shè)置中。它被其它contrib
包(比如authentication
框架和admin
應(yīng)用)使用。
contenttypes
應(yīng)用包括一個(gè)ContentType
模型。這個(gè)模型的實(shí)例代表你的應(yīng)用中的真實(shí)模型,當(dāng)你的項(xiàng)目中安裝了一個(gè)新模型時(shí),會(huì)自動(dòng)創(chuàng)建一個(gè)新的ContentType
實(shí)例。ContentType
模型包括以下字段:
-
app_label
:模型所屬應(yīng)用的名字。它會(huì)自動(dòng)從模型Meta
選項(xiàng)的app_label
屬性中獲得。例如,我們的Image
模型屬于images
應(yīng)用。 -
model
:模型的類名。 -
name
:模型的人性化名字。它自動(dòng)從模型Meta
選項(xiàng)的verbose_name
屬性中獲得。
讓我們看下如何與ContentType
對象交互。使用python manage.py shell
命令打開Python終端。通過執(zhí)行帶label_name
和model
屬性的查詢,你可以獲得指定模型對應(yīng)的ContentType
對象,比如:
>>> from django.contrib.contenttypes.models import ContentType
>>> image_type = ContentType.objects.get(app_label='images',model='image')
>>> image_type
<ContentType: image>
你也可以通過調(diào)用ContentType
對象的model_class()
方法,反向查詢模型類:
>>> image_type.model_class()
<class 'images.models.Image'>
從指定的模型類獲得ContentType
對象操作也很常見:
>>> from images.models import Image
>>> ContentType.objects.get_for_model(Image)
<ContentType: image>
這些只是使用contenttypes
的幾個(gè)示例。Django提供了更多使用它們的方式。你可以在官方文檔學(xué)習(xí)contenttypes
框架。
6.2.2 在模型中添加通用關(guān)系
在通用關(guān)系中,ContentType
對象指向關(guān)系中使用的模型。在模型中設(shè)置通用關(guān)系,你需要三個(gè)字段:
- 一個(gè)
ForeignKey
字段指向ContentType
。這會(huì)告訴我們關(guān)系中的模型。 - 一個(gè)存儲(chǔ)關(guān)聯(lián)對象主鍵的字段。通常這是一個(gè)
PositiveIntegerField
,來匹配Django自動(dòng)生成的主鍵字段。 - 一個(gè)使用上面兩個(gè)字段定義和管理通用關(guān)系的字段。
contenttypes
框架為此定義了GenericForeignKey
字段。
編輯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è)指向ContentType
模型的ForeignKey
字段。 -
target_id
:一個(gè)用于存儲(chǔ)關(guān)聯(lián)對象主鍵的PositiveIntegerField
。 -
target
:一個(gè)指向由前兩個(gè)字段組合的關(guān)聯(lián)對象的GenericForeignKey
字段。
Django不會(huì)在數(shù)據(jù)庫中為GenericForeignKey
字段創(chuàng)建任何字段。只有target_ct
和target_id
字段會(huì)映射到數(shù)據(jù)庫的字段。因?yàn)檫@兩個(gè)字段都有blank=True
和null=True
屬性,所以保存Action
對象時(shí)target
對象不是必需的。
使用通用關(guān)系有意義的時(shí)候,你可以使用它代替外鍵,讓應(yīng)用更靈活。
執(zhí)行以下命令為這個(gè)應(yīng)用創(chuàng)建初始的數(shù)據(jù)庫遷移:
python manage.py makemigrations actions
你會(huì)看到以下輸出:
Migrations for 'actions':
actions/migrations/0001_initial.py
- Create model Action
接著執(zhí)行以下命令同步應(yīng)用和數(shù)據(jù)庫:
python manage.py migrate
這個(gè)命令的輸入表示新的數(shù)據(jù)庫遷移已經(jīng)生效:
Applying actions.0001_initial... OK
當(dāng)我們把Action
模型添加到管理站點(diǎn)。編輯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)
你剛剛在管理站點(diǎn)注冊了Action
模型。執(zhí)行python manage.py runserver
命令啟動(dòng)開服務(wù)器,然后在瀏覽器中打開http://127.0.0.1:8000/actions/action/add/
。你會(huì)看到創(chuàng)建一個(gè)新的Action
對象的頁面,如下圖所示:
正如你所看到的,只有target_ct
和target_id
字段映射到實(shí)際的數(shù)據(jù)庫字段,而GenericForeignKey
沒有在這里出現(xiàn)。target_ct
允許你選擇在Django項(xiàng)目中注冊的任何模型。使用target_ct
字段的limit_choices_to
屬性,可以讓contenttypes
從一個(gè)限制的模型集合中選擇:limit_choices_to
屬性允許你限制ForeignKey
字段的內(nèi)容為一組指定的值。
在actions
應(yīng)用目錄中創(chuàng)建一個(gè)utils.py
文件。我們將定義一些快捷方法,快速創(chuàng)建Action
對象。編輯這個(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()
方法允許我們創(chuàng)建Action
對象,其中包括一個(gè)可選的target
對象。我們可以在任何地方使用這個(gè)函數(shù)添加新操作到活動(dòng)流中。
6.2.3 避免活動(dòng)流中的重復(fù)操作
有時(shí)候用戶可能執(zhí)行一個(gè)操作多次。他們可能在很短的時(shí)間內(nèi)多次點(diǎn)擊like/unlike
按鈕,或者執(zhí)行同一個(gè)操作多次。最終會(huì)讓你存儲(chǔ)和顯示重復(fù)操作。為了避免這種情況,我們會(huì)完善create_acion()
函數(shù),避免大部分重復(fù)操作。
編輯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, targt_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ù)操作,并返回一個(gè)布爾值,表示操作是否保存。我們是這樣避免重復(fù)的:
- 首先使用Django提供的
timezone.now()
方法獲得當(dāng)前時(shí)間。這個(gè)函數(shù)的作用與datetime.datetime.now()
相同,但它返回一個(gè)timezone-aware
對象。Django提供了一個(gè)USE_TZ
設(shè)置,用于啟用或禁止時(shí)區(qū)支持。使用startproject
命令創(chuàng)建的默認(rèn)settings.py
文件中,包括USE_TZ=True
。 - 我們使用
last_minute
變量存儲(chǔ)一分鐘之前的時(shí)間,然后我們檢索用戶從那之后執(zhí)行的所有相同操作。 - 如果最后一分鐘沒有相同的操作,則創(chuàng)建一個(gè)
Action
對象。如果創(chuàng)建了Action
對象,則返回True
,否則返回False
。
6.2.4 添加用戶操作到活動(dòng)流中
是時(shí)候?yàn)橛脩籼砑右恍┎僮鞯揭晥D中,來創(chuàng)建活動(dòng)流了。我們將為以下幾種交互存儲(chǔ)操作:
- 用戶標(biāo)記圖片
- 用戶喜歡或不喜歡一張圖片
- 用戶創(chuàng)建賬戶
- 用戶關(guān)注或取消關(guān)注其它用戶
編輯images
應(yīng)用的views.py
文件,添加以下導(dǎo)入:
from actions.utils import create_action
在image_create
視圖中,在保存圖片之后添加create_action()
:
new_item.save()
create_action(request.user, 'bookmarked image', new_item)
在image_like
視圖中,在添加用戶到users_like
關(guān)系之后添加create_action()
:
image.users_like.add(request.user)
create_action(request.user, 'likes', image)
現(xiàn)在編輯account
應(yīng)用的views.py
文件,添加以下導(dǎo)入:
from actions.utils import create_action
在register
視圖中,在創(chuàng)建Profile
對象之后添加create_action()
:
new_user.save()
profile = Profile.objects.create(user=new_user)
create_action(new_user, 'has created an account')
在user_follow
視圖中,添加create_action()
:
Contact.objects.get_or_create(user_from=request.user, user_to=user)
create_action(request.user, 'is following', user)
正如你所看到的,多虧了Action
模型和幫助函數(shù),讓我們很容易的在活動(dòng)流中保存新操作。
6.2.5 顯示活動(dòng)流
最后,我們需要為每個(gè)用戶顯示活動(dòng)流。我們將把它包括在用戶的儀表盤中。編輯account
應(yīng)用的views.py
文件。導(dǎo)入Action
模型,并修改dashboard
視圖:
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è)視圖中,我們從數(shù)據(jù)庫中檢索當(dāng)前用戶之外的所有用戶執(zhí)行的操作。如果用戶還沒有關(guān)注任何人,我們顯示其它用戶最近的操作。這是用戶沒有關(guān)注其他用戶時(shí)的默認(rèn)行為。如果用戶關(guān)注了其他用戶,我們限制查詢只顯示他關(guān)注的用戶執(zhí)行的操作。最后,我們限制只返回前10個(gè)操作。在這里沒有使用order_by()
進(jìn)行排序,因?yàn)槲覀兪褂?code>Action模型的Meta
選項(xiàng)提供的默認(rèn)排序。因?yàn)槲覀冊?code>Action模型中設(shè)置了ordering = ('-created',)
,所以會(huì)先返回最新的操作。
6.2.6 優(yōu)化涉及關(guān)聯(lián)對象的QuerySet
每次檢索一個(gè)Action
對象時(shí),你可能需要訪問與它關(guān)聯(lián)的User
對象,以及該用戶關(guān)聯(lián)的Profile
對象。Django ORM提供了一種方式,可以一次檢索關(guān)聯(lián)對象,避免額外的數(shù)據(jù)庫查詢。
6.2.6.1 使用select_related
Django提供了一個(gè)select_related
方法,允許你檢索一對多關(guān)系的關(guān)聯(lián)對象。它會(huì)轉(zhuǎn)換為單個(gè)更復(fù)雜的QuerySet
,但是訪問關(guān)聯(lián)對象時(shí),可以避免額外的查詢。select_related
方法用于ForeignKey
和OneToOne
字段。它在SELECT
語句中執(zhí)行SQL JOIN
,并且包括了關(guān)聯(lián)對象的字段。
要使用select_related()
,需要編輯之前代碼的這一行:
actions = actions.filter(user_id__in=following_ids)
并在你會(huì)使用的字段上添加select_related
:
actions = actions.filter(user_id__in=following_ids)\
.select_related('user', 'user__profile')
我們用user__profile
在單條SQL查詢中連接了Profile
表。如果調(diào)用select_related()
時(shí)沒有傳遞參數(shù),那么它會(huì)從所有ForeignKey
關(guān)系中檢索對象。總是將之后會(huì)訪問的關(guān)系限制為select_related()
。
仔細(xì)使用
select_related()
可以大大減少執(zhí)行時(shí)間。
6.2.6.2 使用prefetch_related
正如你所看到的,在一對多關(guān)系中檢索關(guān)聯(lián)對象時(shí),select_related()
會(huì)提高執(zhí)行效率。但是select_related()
不能用于多對多或者多對一關(guān)系。Django提供了一個(gè)名為prefetch_related
的QuerySet
方法,除了select_related()
支持的關(guān)系之外,還可以用于多對多和多對一關(guān)系。prefetch_related()
方法為每個(gè)關(guān)系執(zhí)行獨(dú)立的查詢,然后用Python連接結(jié)果。該方法還支持GenericRelation
和GenericForeignKey
的預(yù)讀取。
為GenericForeignKey
字段target
添加prefetch_related()
,完成這個(gè)查詢:
actions = actions.filter(user_id__in=following_ids)\
.select_related('user', 'user__profile')\
.prefetch_related('target')
現(xiàn)在查詢已經(jīng)優(yōu)化,用于檢索包括關(guān)聯(lián)對象的用戶操作。
6.2.7 為操作創(chuàng)建模板
我們將創(chuàng)建模板用于顯示特定的Action
對象。在actions
應(yīng)用目錄下創(chuàng)建templates
目錄,并添加以下文件結(jié)構(gòu):
actions/
action/
detail.html
編輯actions/action/detail.html
目錄文件,并添加以下代碼:
{% 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 }}">

</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 }}">

</a>
{% endthumbnail %}
{% endif %}
{% endwith %}
{% endif %}
</div>
<div class="info">
<p>
<span class="date">{{ action.created|timesince }} age</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 %}
這是顯示一個(gè)Action
對象的模板。首先,我們使用{% with %}
模板標(biāo)簽檢索執(zhí)行操作的用戶和他們的個(gè)人資料。接著,如果Action
對象有關(guān)聯(lián)的target
對象,則顯示target
對象的圖片。最后,我們顯示執(zhí)行操作的用戶鏈接,描述,以及target
對象(如果有的話)。
現(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>
在瀏覽器中打開http://127.0.0.1:8000/account/
。用已存在的用戶登錄,并執(zhí)行一些操作存儲(chǔ)在數(shù)據(jù)庫中。接著用另一個(gè)用戶登錄,并關(guān)注之前那個(gè)用戶,然后在儀表盤頁面查看生成的活動(dòng)流,如下圖所示:
我們?yōu)橛脩魟?chuàng)建了一個(gè)完整的活動(dòng)流,并且能很容易的添加新的用戶操作。你還可以通過AJAX分頁,在活動(dòng)流中添加無限滾動(dòng),就像我們在image_list
視圖中那樣。
6.3 使用信號(hào)進(jìn)行反規(guī)范化計(jì)數(shù)
某些情況下你希望對數(shù)據(jù)進(jìn)行反規(guī)范化處理。反規(guī)范化(denormalization)是在一定程度上制造一些冗余數(shù)據(jù),從而優(yōu)化讀取性能。你必須小心使用反規(guī)范化,只有當(dāng)你真的需要的時(shí)候才使用。反規(guī)范化最大的問題是很難保持?jǐn)?shù)據(jù)的更新。
我們將通過一個(gè)例子解釋如何通過反規(guī)范化計(jì)數(shù)來改善查詢。缺點(diǎn)是我們必須保持冗余數(shù)據(jù)的更新。我們將在Image
模型中使用反規(guī)范數(shù)據(jù),并使用Django的信號(hào)來保持?jǐn)?shù)據(jù)的更新。
6.3.1 使用信號(hào)
Django自帶一個(gè)信號(hào)調(diào)度程序,當(dāng)特定動(dòng)作發(fā)生時(shí),允許接收函數(shù)獲取通知。當(dāng)某些事情發(fā)生時(shí),你的代碼需要完成某些工作,信號(hào)非常有用。你也可以創(chuàng)建自己的信號(hào),當(dāng)事件發(fā)生時(shí),其他人可以獲得通知。
Django在django.db.models.signals
中為模型提供了幾種信號(hào),其中包括:
-
pre_save
和post_save
:調(diào)用模型的save()
方法之前或之后發(fā)送 -
pre_delete
和post_delete
:調(diào)用模型或QuerySet
的delete()
方法之前或之后發(fā)送 -
m2m_changed
:當(dāng)模型的ManyToManyField
改變時(shí)發(fā)送
這只是Django提供了部分信號(hào)。你可以在這里查看Django的所有內(nèi)置信號(hào)。
我們假設(shè)你想獲取熱門圖片。你可以使用Django聚合函數(shù),按用戶喜歡數(shù)量進(jìn)行排序。記住你已經(jīng)在第三章中使用了聚合函數(shù)。以下代碼按喜歡數(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ù)量比直接使用一個(gè)存儲(chǔ)喜歡數(shù)量的字段更費(fèi)時(shí)。你可以在Image
模型中添加一個(gè)字段,用來反規(guī)范化喜歡數(shù)量,從而提高涉及這個(gè)字段的查詢性能。如何保持這個(gè)字段的更新呢?
編輯images
應(yīng)用的models.py
文件,為Image
模型添加以下字段:
total_likes = models.PositiveIntegerField(db_index=True, default=0)
total_likes
字段允許我們存儲(chǔ)每張圖片被用戶喜歡的數(shù)量。當(dāng)你希望過濾或者排序QuerySet
時(shí),反規(guī)范計(jì)數(shù)非常有用。
在使用反規(guī)范字段之前,你必須考慮其它提升性能的方式。比如數(shù)據(jù)庫索引,查詢優(yōu)化和緩存。
執(zhí)行以下命令為新添加的字段創(chuàng)建數(shù)據(jù)庫遷移:
python manage.py makemigrations images
你會(huì)看到以下輸出:
Migrations for 'images':
images/migrations/0002_image_total_likes.py
- Add field total_likes to image
接著執(zhí)行以下命令讓遷移生效:
python manage.py migrate images
輸出中會(huì)包括這一行:
Applying images.0002_image_total_likes... OK
我們將會(huì)為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ù)為receiver()
函數(shù),并把它附加給m2m_changed
信號(hào)。我們把函數(shù)連接到Image.users_like.throuth
,只有這個(gè)發(fā)送者發(fā)起m2m_changed
信號(hào)時(shí),這個(gè)方法才會(huì)被調(diào)用。還可以使用Signal
對象的connect()
方法來注冊receiver()
函數(shù)。
Django信號(hào)是同步和阻塞的。不要用異步任務(wù)導(dǎo)致信號(hào)混亂。但是,當(dāng)你的代碼從信號(hào)中獲得通知時(shí),你可以組合兩者來啟動(dòng)異步任務(wù)。
你必須把接收器函數(shù)連接到一個(gè)信號(hào),這樣每次發(fā)送信號(hào)時(shí),接收器函數(shù)才會(huì)調(diào)用。注冊信號(hào)的推薦方式是在應(yīng)用配置類的ready()
函數(shù)中導(dǎo)入它們。Django提供了一個(gè)應(yīng)用注冊表,用于配置和內(nèi)省應(yīng)用。
6.3.2 定義應(yīng)用配置類
Django允許你為應(yīng)用指定配置類。要為應(yīng)用提供一個(gè)自定義配置,你需要?jiǎng)?chuàng)建一個(gè)自定義類,它繼承自位于django.apps
中的AppConfig
類。應(yīng)用配置類允許為應(yīng)用存儲(chǔ)元數(shù)據(jù)和配置,并提供內(nèi)省。
你可以在這里閱讀更多關(guān)于應(yīng)用配置的信息。
為了注冊你的信號(hào)接收函數(shù),當(dāng)你使用receiver()
裝飾器時(shí),你只需要在AppConfig
類的ready()
方法中導(dǎo)入應(yīng)用的信號(hào)模塊。一旦應(yīng)用注冊表完全填充,就會(huì)調(diào)用這個(gè)方法。這個(gè)方法中應(yīng)該包括應(yīng)用的所有初始化工作。
在images
應(yīng)用目錄下創(chuàng)建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
譯者注:Django 1.11版本中,默認(rèn)已經(jīng)生成了
apps.py
文件,只需要在其中添加ready()
方法。
其中,name
屬性定義應(yīng)用的完整Python路徑;verbose_name
屬性設(shè)置應(yīng)用的可讀名字。它會(huì)在管理站點(diǎn)中顯示。我們在ready()
方法中導(dǎo)入該應(yīng)用的信號(hào)。
現(xiàn)在我們需要告訴Django應(yīng)用配置的位置。編輯images
應(yīng)用目錄的__init__.py
文件,添加這一行代碼:
default_app_config = 'images.apps.ImagesConfig'
在瀏覽器中查看圖片詳情頁面,并點(diǎn)擊like
按鈕。然后回到管理站點(diǎn)查看total_likes
屬性。你會(huì)看到total_likes
已經(jīng)更新,如下圖所示:
現(xiàn)在你可以使用total_likes
屬性按熱門排序圖片,或者在任何地方顯示它,避免了用復(fù)雜的查詢來計(jì)算。以下按圖片被喜歡的總數(shù)量排序的查詢:
images_by_popularity = Image.objects.annotate(likes=Count('users_like')).order_by('-likes')
可以變?yōu)檫@樣:
images_by_popularity = Image.objects.order_by('-total_likes')
通過更快的SQL查詢就返回了這個(gè)結(jié)果。這只是使用Django信號(hào)的一個(gè)示例。
小心使用信號(hào),因?yàn)樗鼤?huì)讓控制流更難理解。如果你知道會(huì)通知哪個(gè)接收器,很多情況下就能避免使用信號(hào)。
你需要設(shè)置初始計(jì)數(shù),來匹配數(shù)據(jù)庫的當(dāng)前狀態(tài)。使用python manage.py shell
命令打開終端,執(zhí)行以下命令:
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)更新了。
6.4 用Redis存儲(chǔ)項(xiàng)視圖
Redis是一個(gè)高級(jí)的鍵值對數(shù)據(jù)庫,允許你存儲(chǔ)不同類型的數(shù)據(jù),并且能進(jìn)行非常快速的I/O操作。Redis在內(nèi)存中存儲(chǔ)所有數(shù)據(jù),但數(shù)據(jù)集可以一次性持久化到硬盤中,或者添加每條命令到日志中。與其它鍵值對存儲(chǔ)相比,Redis更通用:它提供了一組功能強(qiáng)大的命令,并支持各種各樣的數(shù)據(jù)結(jié)構(gòu),比如strings
,hashes
,lists
,sets
,ordered sets
,甚至bitmaps
或HyperLogLogs
。
SQL最適合于模式定義的持久化數(shù)據(jù)存儲(chǔ),而當(dāng)處理快速變化的數(shù)據(jù),短暫的存儲(chǔ),或者快速緩存時(shí),Redis有更多的優(yōu)勢。讓我們看看如何使用Redis為我們的項(xiàng)目添加新功能。
6.4.1 安裝Redis
從這里下載最新的Redis版本。解壓tar.gz
文件,進(jìn)入redis
目錄,使用make
命令編譯Redis:
cd redis-3.2.8
make
安裝完成后,使用以下命令初始化Redis服務(wù)器:
src/redis-server
你會(huì)看到結(jié)尾的輸出為:
19608:M 08 May 17:04:38.217 # Server started, Redis version 3.2.8
19608:M 08 May 17:04:38.217 * The server is now ready to accept connections on port 6379
默認(rèn)情況下,Redis在6379端口運(yùn)行,但你可以使用--port
之指定自定義端口,比如:redis-server --port 6655
。服務(wù)器就緒后,使用以下命令在另一個(gè)終端打開Redis客戶端:
src/redis-cli
你會(huì)看到Redis客戶端終端:
127.0.0.1:6379>
你可以直接在Redis客戶端執(zhí)行Redis命令。讓我們嘗試一下。在Redis終端輸入SET
命令,在鍵中存儲(chǔ)一個(gè)值:
127.0.0.1:6379> SET name "Peter"
OK
以上命令在Redis數(shù)據(jù)庫中創(chuàng)建了一個(gè)字符串值為Peter
的name
鍵。輸出OK
表示鍵已經(jīng)成功保存。接收,使用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í)間,這個(gè)命令允許你設(shè)置鍵的存活秒數(shù)。另一個(gè)選項(xiàng)是使用EXPIREAT
命令,它接收一個(gè)Unix時(shí)間戳。把Redis作為緩存,或者存儲(chǔ)臨時(shí)數(shù)據(jù)時(shí),鍵過期非常有用:
127.0.0.1:6379> EXPIRE name 2
(integer) 1
等待2秒,再次獲取同樣的鍵:
127.0.0.1:6379> GET name
(nil)
返回值(nil)
是一個(gè)空返回,表示沒有找到鍵。你也可以使用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ù)類型(比如strings
,hashes
,lists
,sets
,ordered sets
等等)提供了大量的命令。你可以在這里查看所有Redis命令,在這里查看所有Redis數(shù)據(jù)類型。
6.4.2 在Python中使用Redis
我們需要為Redis綁定Python。通過pip
安裝redis-py
:
pip install redis
你可以在這里查看redis-py
的文檔。
redis-py
提供了兩個(gè)類用于與Redis交互:StricRedis
和Redis
。兩個(gè)類提供了相同的功能。StricRedis
類視圖遵守官方Redis命令語法。Redis
類繼承自StricRedis
,覆寫了一些方法,提供向后的兼容性。我們將使用StrictRedis
類,因?yàn)樗裱璕edis命令語法。打開Python終端,執(zhí)行以下命令:
>>> import redis
>>> r = redis.StrictRedis(host='localhost', port=6379, db=0)
這段代碼創(chuàng)建了一個(gè)Redis數(shù)據(jù)連接。在Redis中,數(shù)據(jù)由整數(shù)索引區(qū)分,而不是數(shù)據(jù)庫名。默認(rèn)情況下,客戶端連接到數(shù)據(jù)庫0。Redis數(shù)據(jù)庫有效的數(shù)字到16,但你可以在redis.conf
文件中修改這個(gè)值。
現(xiàn)在使用Python終端設(shè)置一個(gè)鍵:
>>> r.set('foo', 'bar')
True
命令返回True
表示鍵創(chuàng)建成功。現(xiàn)在你可以使用get()
命令查詢鍵:
>>> r.get('foo')
b'bar'
正如你鎖看到的,StrictRedis
方法遵循Redis命令語法。
讓我們在項(xiàng)目中集成Redis。編輯bookmarks
項(xiàng)目的settings.py
文件,添加以下設(shè)置:
REDIS_HOST = 'localhost'
REDIS_PORT = 6379
REDIS_DB = 0
以上設(shè)置了Redis服務(wù)器和我們在項(xiàng)目中使用的數(shù)據(jù)庫。
6.4.3 在Redis中存儲(chǔ)項(xiàng)的瀏覽次數(shù)
讓我們存儲(chǔ)一張圖片被查看的總次數(shù)。如果我們使用Django ORM,則每次顯示圖片后,都會(huì)涉及UPDATE
語句。如果使用Redis,我們只需要增加內(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連接,以便在視圖中使用。修改image_detail
視圖,如下所示:
def image_detail(request, id, slug):
image = get_object_or_404(Image, id=id, slug=slug)
# increament 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
命令把一個(gè)鍵的值加1,如果鍵不存在,則在執(zhí)行操作之前設(shè)置值為0。incr()
方法返回執(zhí)行操作之后鍵的值,我們把它存在total_views
變量中。我們用object-type:id:field
(比如image:33:id:views
)構(gòu)建Redis鍵。
Redis鍵的命名慣例是使用冒號(hào)分割,來創(chuàng)建帶命名空間的鍵。這樣鍵名會(huì)很詳細(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)在在瀏覽器中打開圖片詳情頁面,加載多次。你會(huì)看到每次執(zhí)行視圖,顯示的瀏覽總數(shù)都會(huì)加1,如下圖所示:
你已經(jīng)成功的在項(xiàng)目集成了Redis,來存儲(chǔ)項(xiàng)的瀏覽次數(shù)。
6.4.4 在Redis中存儲(chǔ)排名
讓我們用Redis構(gòu)建更多功能。我們將創(chuàng)建瀏覽次數(shù)最多的圖片排名。我們將使用Redis的sorted set
來構(gòu)建排名。一個(gè)sorted set
是一個(gè)不重復(fù)的字符串集合,每個(gè)成員關(guān)聯(lián)一個(gè)分?jǐn)?shù)。項(xiàng)通過它們的分?jǐn)?shù)存儲(chǔ)。
編輯images
應(yīng)用的views.py
文件,修改image_detail
視圖,如下所示:
def image_detail(request, id, slug):
image = get_object_or_404(Image, id=id, slug=slug)
# increament total image views by 1
total_views = r.incr('image:{}:views'.format(image.id))
# increament 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()
命令在sorted set
中存儲(chǔ)圖片瀏覽次數(shù),其中鍵為image_ranking
。我們存儲(chǔ)圖片id,分?jǐn)?shù)1會(huì)被加到sorted set
中這個(gè)元素的總分上。這樣就可以全局追蹤所有圖片的瀏覽次數(shù),并且有一個(gè)按瀏覽次數(shù)排序的sorted set
。
現(xiàn)在創(chuàng)建一個(gè)新視圖,用于顯示瀏覽次數(shù)最多的圖片排名。在views.py
文件中添加以下代碼:
@login_required
def image_ranking(request):
# get image ranking dictinary
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()
命令獲得sorted set
中的元素。這個(gè)命令通過最低和最高分指定自定義范圍。通過0作為最低,-1作為最高分,我們告訴Redis返回sorted set
中的所有元素。我們還指定desc=True
,按分?jǐn)?shù)的降序排列返回元素。最后,我們用[:10]
切片操作返回分?jǐn)?shù)最高的前10個(gè)元素。我們構(gòu)建了一個(gè)返回的圖片ID列表,并作為整數(shù)列表存在image_ranking_ids
變量中。我們迭代這些ID的Image
對象,并使用list()
函數(shù)強(qiáng)制執(zhí)行查詢。強(qiáng)制QuerySet
執(zhí)行很重要,因?yàn)橹笪覀円{(diào)用列表的sort()
方法(此時(shí)我們需要一組對象,而不是一個(gè)QuerySet
)。我們通過Image
對象在圖片排名中的索引進(jìn)行排序。現(xiàn)在我們可以在模板中使用most_viewed
列表顯示瀏覽次數(shù)最多的前10張圖片。
創(chuàng)建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_absolute_url }}">
{{ image.title }}
</a>
</li>
{% endfor %}
</ol>
{% endblock %}
這個(gè)模板非常簡單,我們迭代most_viewed
列表中的Image
對象。
最后為新視圖創(chuàng)建URL模式。編輯images
應(yīng)用的urls.py
文件,添加以下模式:
url(r'^/ranking/$', views.image_ranking, name='ranking')
在瀏覽器中打開http://127.0.0.1:8000/images/ranking/
,你會(huì)看到圖片排名,如下圖所示:
6.4.5 Redis的后續(xù)功能
Redis不是SQL數(shù)據(jù)庫的替代者,而是更適用于特定任務(wù)的快速的內(nèi)存存儲(chǔ)。當(dāng)你真的需要時(shí)可以使用它。Redis非常適合以下場景:
- 計(jì)數(shù):正如你所看到的,使用Redis管理計(jì)算非常簡單。你可以使用
incr()
和incrby()
計(jì)數(shù)。 - 存儲(chǔ)最近的項(xiàng):你可以使用
lpush()
和rpush()
在列表開頭或結(jié)尾添加項(xiàng)。使用lpop()
或rpop()
移除并返回第一或最后一項(xiàng)。你可以使用ltrim()
截?cái)嗔斜黹L度。 - 隊(duì)列:除了
push
和pop
命令,Redis還提供了阻塞隊(duì)列的命令。 - 緩存:使用
expire()
和expireat()
允許你把Redis當(dāng)做緩存。你還可以找到Django的第三方Redis緩存后臺(tái)。 - 訂閱/發(fā)布:Redis還為訂閱/取消訂閱,以及發(fā)送消息給頻道提供了命令。
- 排名和排行榜:Redis的
sorted set
可以很容易創(chuàng)建排行榜。 - 實(shí)時(shí)跟蹤:Redis的快速I/O非常適合實(shí)時(shí)場景。
6.5 總結(jié)
這一章中,你構(gòu)建了關(guān)注系統(tǒng)和用戶活動(dòng)流。你學(xué)習(xí)了Django信號(hào)是如何工作的,并在項(xiàng)目中集成了Redis。
下一章中,你會(huì)學(xué)習(xí)如何構(gòu)建一個(gè)在線商店。你將創(chuàng)建一個(gè)產(chǎn)品目錄,并使用會(huì)話構(gòu)建購物車。你還講學(xué)習(xí)如何使用Celery啟動(dòng)異步任務(wù)。