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

編輯account應用的account/user/detail.html模板,添加以下代碼:
{% extends "base.html" %}
{% load thumbnail %}
{% block title %}{{ user.get_full_name }}{% endblock %}
{% block content %}
<h1>{{ user.get_full_name }}</h1>
<div class="profile-info">
{% thumbnail user.profile.photo "180x180" crop="100%" as im %}

{% endthumbnail %}
</div>
{% with total_followers=user.followers.count %}
<span class="count">
<span class="total">{{ total_followers }}</span>
follower{{ total_followers|pluralize }}
</span>
<a href="#" data-id="{{ user.id }}"
data-action="{% if request.user in user.followers.all %}un{% endif %}follow"
class="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 %}
我們在詳情模板中顯示用戶個人資料,并使用{% thumbnail %}模板標簽顯示個人資料圖片。我們顯示關注者總數(shù)和一個用于follow/unfollow的鏈接。如果用戶正在查看自己的個人資料,我們會隱藏該鏈接,防止用戶關注自己。我們將執(zhí)行AJAX請求來follow/unfollow指定用戶。我們在<a>元素中添加data-id和data-action屬性,其中分別包括用戶ID和點擊鏈接時執(zhí)行的操作(關注或取消關注),這取決于請求該頁面的用戶是否已經(jīng)關注了這個用戶。我們用list_ajax.html模板顯示這個用戶標記過的圖片。
再次打開瀏覽器,點擊標記過一些圖片的用戶。你會看到個人資料詳情,如下圖所示:

6.1.3 構建關注用戶的AJAX視圖
我們將使用AJAX創(chuàng)建一個簡單視圖,用于關注或取消關注用戶。編輯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ōu)橛脩舻亩鄬Χ嚓P系使用了自定義的中介模型,所以ManyToManyField自動生成的管理器的默認add()和remove()方法不可用了。我們使用Contact中介模型創(chuàng)建或刪除用戶關系。
在account應用的urls.py文件中導入你剛創(chuàng)建的視圖,然后添加以下URL模式:
url(r'^users/follow/$', views.user_follow, name='user_follow'),
確保你把這個模式放在user_detail模式之前。否則任何到/users/follow/的請求都會匹配user_detail模式的正則表達式,然后執(zhí)行user_detail視圖。記住,每一個HTTP請求時,Django會按每個模式出現(xiàn)的先后順序匹配請求的URL,并在第一次匹配成功后停止。
編輯account應用的user/detail.html模板,添加以下代碼:
{% block domready %}
$('a.follow').click(function(e){
e.preventDefault();
$.post('{% url "user_follow" %}', {
id: $(this).data('id'),
action: $(this).data('action')
},
function(data){
if (data['status'] == 'ok') {
var previous_action = $('a.follow').data('action');
// toggle data-action
$('a.follow').data('action', previous_action == 'follow' ? 'unfollow' : 'follow');
// toggle link text
$('a.follow').text(previous_action == 'follow' ? 'Unfollow' : 'Follow');
// update total followers
var previous_followers = parseInt($('span.count .total').text())
$('span.count .total').text(previous_action == 'follow' ? previous_followers+1 : previous_followers - 1);
}
});
});
{% endblock %}
這段JavaScript代碼執(zhí)行關注或取消關注指定用戶的AJAX請求,同時切換follow/unfollow鏈接。我們用jQuery執(zhí)行AJAX請求,并根據(jù)之前的值設置data-action屬性和<a>元素的文本。AJAX操作執(zhí)行完成后,我們更新頁面顯示的關注總數(shù)。打開一個已存在用戶的詳情頁面,點擊FOLLOW鏈接,測試我們剛添加的功能。
6.2 構建通用的活動流應用
很多社交網(wǎng)站都會給用戶顯示活動流,讓用戶可以跟蹤其他用戶在平臺上做了什么?;顒恿魇且粋€或一組用戶最近執(zhí)行的活動列表。比如,F(xiàn)acebook的News Feed就是一個活動流。又或者用戶X標記了圖片Y,或者用戶X不再關注用戶Y。我們將構造一個活動流應用,讓每個用戶都可以看到他關注的用戶最近的操作。要實現(xiàn)這個功能,我們需要一個模型,存儲用戶在網(wǎng)站中執(zhí)行的操作,并提供簡單的添加操作的方式。
用以下命令在項目中創(chuàng)建actions應用:
django-admin startapp actions
在項目的settings.py文件的INSTALLED_APPS中添加actions,讓Django知道新應用已經(jīng)激活:
INSTALLED_APPS = (
# ...
'actions',
)
編輯actions應用的models.py文件,添加以下代碼:
from django.db import models
from django.contrib.auth.models import User
class Action(models.Model):
user = models.ForeignKey(User, related_name='actions', db_index=True)
verb = models.CharField(max_length=255)
created = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta:
ordering = ('-created', )
這是Actioin模型,用于存儲用戶活動。該模型的字段有:
-
user:執(zhí)行這個操作的用戶。這是一個指向Django的User模型的ForeignKey。 -
verb:描述用戶執(zhí)行的操作。 -
created:該操作創(chuàng)建的日期和時間。我們使用auto_now_add=True自動設置為對象第一次在數(shù)據(jù)庫中保存的時間。
通過這個基礎模型,我們只能存儲類似用戶X做了某些事情的操作。我們需要一個額外的ForeignKey字段,存儲涉及目標對象的操作,比如用戶X標記了圖片Y,或者用戶X關注了用戶Y。你已經(jīng)知道,一個普通的ForeignKey字段只能指向另一個模型。但是我們需要一種方式,讓操作的目標對象可以是任何一個已經(jīng)存在的模型的實例。這就是Django的contenttypes框架的作用。
6.2.1 使用contenttypes框架
Django的contenttypes框架位于django.contrib.contenttypes中。這個應用可以跟蹤項目中安裝的所有模型,并提供一個通用的接口與模型交互。
當你使用startproject命令創(chuàng)建新項目時,django.contrib.contenttypes已經(jīng)包括在INSTALLED_APPS設置中。它被其它contrib包(比如authentication框架和admin應用)使用。
contenttypes應用包括一個ContentType模型。這個模型的實例代表你的應用中的真實模型,當你的項目中安裝了一個新模型時,會自動創(chuàng)建一個新的ContentType實例。ContentType模型包括以下字段:
-
app_label:模型所屬應用的名字。它會自動從模型Meta選項的app_label屬性中獲得。例如,我們的Image模型屬于images應用。 -
model:模型的類名。 -
name:模型的人性化名字。它自動從模型Meta選項的verbose_name屬性中獲得。
讓我們看下如何與ContentType對象交互。使用python manage.py shell命令打開Python終端。通過執(zhí)行帶label_name和model屬性的查詢,你可以獲得指定模型對應的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的幾個示例。Django提供了更多使用它們的方式。你可以在官方文檔學習contenttypes框架。
6.2.2 在模型中添加通用關系
在通用關系中,ContentType對象指向關系中使用的模型。在模型中設置通用關系,你需要三個字段:
- 一個
ForeignKey字段指向ContentType。這會告訴我們關系中的模型。 - 一個存儲關聯(lián)對象主鍵的字段。通常這是一個
PositiveIntegerField,來匹配Django自動生成的主鍵字段。 - 一個使用上面兩個字段定義和管理通用關系的字段。
contenttypes框架為此定義了GenericForeignKey字段。
編輯actions應用的models.py文件,如下所示:
from django.db import models
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
class Action(models.Model):
user = models.ForeignKey(User, related_name='actions', db_index=True)
verb = models.CharField(max_length=255)
target_ct = models.ForeignKey(ContentType, blank=True, null=True, related_name='target_obj')
target_id = models.PositiveIntegerField(null=True, blank=True, db_index=True)
target = GenericForeignKey('target_ct', 'target_id')
created = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta:
ordering = ('-created', )
我們在Action模型中添加了以下字段:
-
target_ct:一個指向ContentType模型的ForeignKey字段。 -
target_id:一個用于存儲關聯(lián)對象主鍵的PositiveIntegerField。 -
target:一個指向由前兩個字段組合的關聯(lián)對象的GenericForeignKey字段。
Django不會在數(shù)據(jù)庫中為GenericForeignKey字段創(chuàng)建任何字段。只有target_ct和target_id字段會映射到數(shù)據(jù)庫的字段。因為這兩個字段都有blank=True和null=True屬性,所以保存Action對象時target對象不是必需的。
使用通用關系有意義的時候,你可以使用它代替外鍵,讓應用更靈活。
執(zhí)行以下命令為這個應用創(chuàng)建初始的數(shù)據(jù)庫遷移:
python manage.py makemigrations actions
你會看到以下輸出:
Migrations for 'actions':
actions/migrations/0001_initial.py
- Create model Action
接著執(zhí)行以下命令同步應用和數(shù)據(jù)庫:
python manage.py migrate
這個命令的輸入表示新的數(shù)據(jù)庫遷移已經(jīng)生效:
Applying actions.0001_initial... OK
當我們把Action模型添加到管理站點。編輯actions應用的admin.py文件,添加以下代碼:
from django.contrib import admin
from .models import Action
class ActionAdmin(admin.ModelAdmin):
list_display = ('user', 'verb', 'target', 'created')
list_filter = ('created', )
search_fields = ('verb', )
admin.site.register(Action, ActionAdmin)
你剛剛在管理站點注冊了Action模型。執(zhí)行python manage.py runserver命令啟動開服務器,然后在瀏覽器中打開http://127.0.0.1:8000/actions/action/add/。你會看到創(chuàng)建一個新的Action對象的頁面,如下圖所示:

正如你所看到的,只有target_ct和target_id字段映射到實際的數(shù)據(jù)庫字段,而GenericForeignKey沒有在這里出現(xiàn)。target_ct允許你選擇在Django項目中注冊的任何模型。使用target_ct字段的limit_choices_to屬性,可以讓contenttypes從一個限制的模型集合中選擇:limit_choices_to屬性允許你限制ForeignKey字段的內(nèi)容為一組指定的值。
在actions應用目錄中創(chuàng)建一個utils.py文件。我們將定義一些快捷方法,快速創(chuàng)建Action對象。編輯這個新文件,添加以下代碼:
from django.contrib.contenttypes.models import ContentType
from .models import Action
def create_action(user, verb, target=None):
action = Action(user=user, verb=verb, target=target)
action.save()
create_action()方法允許我們創(chuàng)建Action對象,其中包括一個可選的target對象。我們可以在任何地方使用這個函數(shù)添加新操作到活動流中。
6.2.3 避免活動流中的重復操作
有時候用戶可能執(zhí)行一個操作多次。他們可能在很短的時間內(nèi)多次點擊like/unlike按鈕,或者執(zhí)行同一個操作多次。最終會讓你存儲和顯示重復操作。為了避免這種情況,我們會完善create_acion()函數(shù),避免大部分重復操作。
編輯actions應用的utils.py文件,如下所示:
import datetime
from django.utils import timezone
from django.contrib.contenttypes.models import ContentType
from .models import Action
def create_action(user, verb, target=None):
# check for any similar action made in the last minute
now = timezone.now()
last_minute = now - datetime.timedelta(seconds=60)
similar_actions = Action.objects.filter(user_id=user.id, verb=verb, 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ù),避免保存重復操作,并返回一個布爾值,表示操作是否保存。我們是這樣避免重復的:
- 首先使用Django提供的
timezone.now()方法獲得當前時間。這個函數(shù)的作用與datetime.datetime.now()相同,但它返回一個timezone-aware對象。Django提供了一個USE_TZ設置,用于啟用或禁止時區(qū)支持。使用startproject命令創(chuàng)建的默認settings.py文件中,包括USE_TZ=True。 - 我們使用
last_minute變量存儲一分鐘之前的時間,然后我們檢索用戶從那之后執(zhí)行的所有相同操作。 - 如果最后一分鐘沒有相同的操作,則創(chuàng)建一個
Action對象。如果創(chuàng)建了Action對象,則返回True,否則返回False。
6.2.4 添加用戶操作到活動流中
是時候為用戶添加一些操作到視圖中,來創(chuàng)建活動流了。我們將為以下幾種交互存儲操作:
- 用戶標記圖片
- 用戶喜歡或不喜歡一張圖片
- 用戶創(chuàng)建賬戶
- 用戶關注或取消關注其它用戶
編輯images應用的views.py文件,添加以下導入:
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關系之后添加create_action():
image.users_like.add(request.user)
create_action(request.user, 'likes', image)
現(xiàn)在編輯account應用的views.py文件,添加以下導入:
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ù),讓我們很容易的在活動流中保存新操作。
6.2.5 顯示活動流
最后,我們需要為每個用戶顯示活動流。我們將把它包括在用戶的儀表盤中。編輯account應用的views.py文件。導入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})
在這個視圖中,我們從數(shù)據(jù)庫中檢索當前用戶之外的所有用戶執(zhí)行的操作。如果用戶還沒有關注任何人,我們顯示其它用戶最近的操作。這是用戶沒有關注其他用戶時的默認行為。如果用戶關注了其他用戶,我們限制查詢只顯示他關注的用戶執(zhí)行的操作。最后,我們限制只返回前10個操作。在這里沒有使用order_by()進行排序,因為我們使用Action模型的Meta選項提供的默認排序。因為我們在Action模型中設置了ordering = ('-created',),所以會先返回最新的操作。
6.2.6 優(yōu)化涉及關聯(lián)對象的QuerySet
每次檢索一個Action對象時,你可能需要訪問與它關聯(lián)的User對象,以及該用戶關聯(lián)的Profile對象。Django ORM提供了一種方式,可以一次檢索關聯(lián)對象,避免額外的數(shù)據(jù)庫查詢。
6.2.6.1 使用select_related
Django提供了一個select_related方法,允許你檢索一對多關系的關聯(lián)對象。它會轉換為單個更復雜的QuerySet,但是訪問關聯(lián)對象時,可以避免額外的查詢。select_related方法用于ForeignKey和OneToOne字段。它在SELECT語句中執(zhí)行SQL JOIN,并且包括了關聯(lián)對象的字段。
要使用select_related(),需要編輯之前代碼的這一行:
actions = actions.filter(user_id__in=following_ids)
并在你會使用的字段上添加select_related:
actions = actions.filter(user_id__in=following_ids)\
.select_related('user', 'user__profile')
我們用user__profile在單條SQL查詢中連接了Profile表。如果調(diào)用select_related()時沒有傳遞參數(shù),那么它會從所有ForeignKey關系中檢索對象。總是將之后會訪問的關系限制為select_related()。
仔細使用
select_related()可以大大減少執(zhí)行時間。
6.2.6.2 使用prefetch_related
正如你所看到的,在一對多關系中檢索關聯(lián)對象時,select_related()會提高執(zhí)行效率。但是select_related()不能用于多對多或者多對一關系。Django提供了一個名為prefetch_related的QuerySet方法,除了select_related()支持的關系之外,還可以用于多對多和多對一關系。prefetch_related()方法為每個關系執(zhí)行獨立的查詢,然后用Python連接結果。該方法還支持GenericRelation和GenericForeignKey的預讀取。
為GenericForeignKey字段target添加prefetch_related(),完成這個查詢:
actions = actions.filter(user_id__in=following_ids)\
.select_related('user', 'user__profile')\
.prefetch_related('target')
現(xiàn)在查詢已經(jīng)優(yōu)化,用于檢索包括關聯(lián)對象的用戶操作。
6.2.7 為操作創(chuàng)建模板
我們將創(chuàng)建模板用于顯示特定的Action對象。在actions應用目錄下創(chuàng)建templates目錄,并添加以下文件結構:
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 %}
這是顯示一個Action對象的模板。首先,我們使用{% with %}模板標簽檢索執(zhí)行操作的用戶和他們的個人資料。接著,如果Action對象有關聯(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í)行一些操作存儲在數(shù)據(jù)庫中。接著用另一個用戶登錄,并關注之前那個用戶,然后在儀表盤頁面查看生成的活動流,如下圖所示:

我們?yōu)橛脩魟?chuàng)建了一個完整的活動流,并且能很容易的添加新的用戶操作。你還可以通過AJAX分頁,在活動流中添加無限滾動,就像我們在image_list視圖中那樣。
6.3 使用信號進行反規(guī)范化計數(shù)
某些情況下你希望對數(shù)據(jù)進行反規(guī)范化處理。反規(guī)范化(denormalization)是在一定程度上制造一些冗余數(shù)據(jù),從而優(yōu)化讀取性能。你必須小心使用反規(guī)范化,只有當你真的需要的時候才使用。反規(guī)范化最大的問題是很難保持數(shù)據(jù)的更新。
我們將通過一個例子解釋如何通過反規(guī)范化計數(shù)來改善查詢。缺點是我們必須保持冗余數(shù)據(jù)的更新。我們將在Image模型中使用反規(guī)范數(shù)據(jù),并使用Django的信號來保持數(shù)據(jù)的更新。
6.3.1 使用信號
Django自帶一個信號調(diào)度程序,當特定動作發(fā)生時,允許接收函數(shù)獲取通知。當某些事情發(fā)生時,你的代碼需要完成某些工作,信號非常有用。你也可以創(chuàng)建自己的信號,當事件發(fā)生時,其他人可以獲得通知。
Django在django.db.models.signals中為模型提供了幾種信號,其中包括:
-
pre_save和post_save:調(diào)用模型的save()方法之前或之后發(fā)送 -
pre_delete和post_delete:調(diào)用模型或QuerySet的delete()方法之前或之后發(fā)送 -
m2m_changed:當模型的ManyToManyField改變時發(fā)送
這只是Django提供了部分信號。你可以在這里查看Django的所有內(nèi)置信號。
我們假設你想獲取熱門圖片。你可以使用Django聚合函數(shù),按用戶喜歡數(shù)量進行排序。記住你已經(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)計圖片的喜歡數(shù)量比直接使用一個存儲喜歡數(shù)量的字段更費時。你可以在Image模型中添加一個字段,用來反規(guī)范化喜歡數(shù)量,從而提高涉及這個字段的查詢性能。如何保持這個字段的更新呢?
編輯images應用的models.py文件,為Image模型添加以下字段:
total_likes = models.PositiveIntegerField(db_index=True, default=0)
total_likes字段允許我們存儲每張圖片被用戶喜歡的數(shù)量。當你希望過濾或者排序QuerySet時,反規(guī)范計數(shù)非常有用。
在使用反規(guī)范字段之前,你必須考慮其它提升性能的方式。比如數(shù)據(jù)庫索引,查詢優(yōu)化和緩存。
執(zhí)行以下命令為新添加的字段創(chuàng)建數(shù)據(jù)庫遷移:
python manage.py makemigrations images
你會看到以下輸出:
Migrations for 'images':
images/migrations/0002_image_total_likes.py
- Add field total_likes to image
接著執(zhí)行以下命令讓遷移生效:
python manage.py migrate images
輸出中會包括這一行:
Applying images.0002_image_total_likes... OK
我們將會為m2m_changed信號附加一個receiver函數(shù)。在images應用目錄下創(chuàng)建一個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信號。我們把函數(shù)連接到Image.users_like.throuth,只有這個發(fā)送者發(fā)起m2m_changed信號時,這個方法才會被調(diào)用。還可以使用Signal對象的connect()方法來注冊receiver()函數(shù)。
Django信號是同步和阻塞的。不要用異步任務導致信號混亂。但是,當你的代碼從信號中獲得通知時,你可以組合兩者來啟動異步任務。
你必須把接收器函數(shù)連接到一個信號,這樣每次發(fā)送信號時,接收器函數(shù)才會調(diào)用。注冊信號的推薦方式是在應用配置類的ready()函數(shù)中導入它們。Django提供了一個應用注冊表,用于配置和內(nèi)省應用。
6.3.2 定義應用配置類
Django允許你為應用指定配置類。要為應用提供一個自定義配置,你需要創(chuàng)建一個自定義類,它繼承自位于django.apps中的AppConfig類。應用配置類允許為應用存儲元數(shù)據(jù)和配置,并提供內(nèi)省。
你可以在這里閱讀更多關于應用配置的信息。
為了注冊你的信號接收函數(shù),當你使用receiver()裝飾器時,你只需要在AppConfig類的ready()方法中導入應用的信號模塊。一旦應用注冊表完全填充,就會調(diào)用這個方法。這個方法中應該包括應用的所有初始化工作。
在images應用目錄下創(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版本中,默認已經(jīng)生成了
apps.py文件,只需要在其中添加ready()方法。
其中,name屬性定義應用的完整Python路徑;verbose_name屬性設置應用的可讀名字。它會在管理站點中顯示。我們在ready()方法中導入該應用的信號。
現(xiàn)在我們需要告訴Django應用配置的位置。編輯images應用目錄的__init__.py文件,添加這一行代碼:
default_app_config = 'images.apps.ImagesConfig'
在瀏覽器中查看圖片詳情頁面,并點擊like按鈕。然后回到管理站點查看total_likes屬性。你會看到total_likes已經(jīng)更新,如下圖所示:

現(xiàn)在你可以使用total_likes屬性按熱門排序圖片,或者在任何地方顯示它,避免了用復雜的查詢來計算。以下按圖片被喜歡的總數(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查詢就返回了這個結果。這只是使用Django信號的一個示例。
小心使用信號,因為它會讓控制流更難理解。如果你知道會通知哪個接收器,很多情況下就能避免使用信號。
你需要設置初始計數(shù),來匹配數(shù)據(jù)庫的當前狀態(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存儲項視圖
Redis是一個高級的鍵值對數(shù)據(jù)庫,允許你存儲不同類型的數(shù)據(jù),并且能進行非??焖俚腎/O操作。Redis在內(nèi)存中存儲所有數(shù)據(jù),但數(shù)據(jù)集可以一次性持久化到硬盤中,或者添加每條命令到日志中。與其它鍵值對存儲相比,Redis更通用:它提供了一組功能強大的命令,并支持各種各樣的數(shù)據(jù)結構,比如strings,hashes,lists,sets,ordered sets,甚至bitmaps或HyperLogLogs。
SQL最適合于模式定義的持久化數(shù)據(jù)存儲,而當處理快速變化的數(shù)據(jù),短暫的存儲,或者快速緩存時,Redis有更多的優(yōu)勢。讓我們看看如何使用Redis為我們的項目添加新功能。
6.4.1 安裝Redis
從這里下載最新的Redis版本。解壓tar.gz文件,進入redis目錄,使用make命令編譯Redis:
cd redis-3.2.8
make
安裝完成后,使用以下命令初始化Redis服務器:
src/redis-server
你會看到結尾的輸出為:
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
默認情況下,Redis在6379端口運行,但你可以使用--port之指定自定義端口,比如:redis-server --port 6655。服務器就緒后,使用以下命令在另一個終端打開Redis客戶端:
src/redis-cli
你會看到Redis客戶端終端:
127.0.0.1:6379>
你可以直接在Redis客戶端執(zhí)行Redis命令。讓我們嘗試一下。在Redis終端輸入SET命令,在鍵中存儲一個值:
127.0.0.1:6379> SET name "Peter"
OK
以上命令在Redis數(shù)據(jù)庫中創(chuàng)建了一個字符串值為Peter的name鍵。輸出OK表示鍵已經(jīng)成功保存。接收,使用GET命令查詢值:
127.0.0.1:6379> GET name
"Peter"
我們也可以使用EXISTS命令檢查一個叫鍵是否存在。如果存在返回1,否則返回0:
127.0.0.1:6379> EXISTS name
(integer) 1
你可以使用EXPIRE命令為鍵設置過期時間,這個命令允許你設置鍵的存活秒數(shù)。另一個選項是使用EXPIREAT命令,它接收一個Unix時間戳。把Redis作為緩存,或者存儲臨時數(shù)據(jù)時,鍵過期非常有用:
127.0.0.1:6379> EXPIRE name 2
(integer) 1
等待2秒,再次獲取同樣的鍵:
127.0.0.1:6379> GET name
(nil)
返回值(nil)是一個空返回,表示沒有找到鍵。你也可以使用DEL命令刪除鍵:
127.0.0.1:6379> SET total 1
OK
127.0.0.1:6379> DEL total
(integer) 1
127.0.0.1:6379> GET total
(nil)
這只是鍵操作的基本命令。Redis為每種數(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提供了兩個類用于與Redis交互:StricRedis和Redis。兩個類提供了相同的功能。StricRedis類視圖遵守官方Redis命令語法。Redis類繼承自StricRedis,覆寫了一些方法,提供向后的兼容性。我們將使用StrictRedis類,因為它遵循Redis命令語法。打開Python終端,執(zhí)行以下命令:
>>> import redis
>>> r = redis.StrictRedis(host='localhost', port=6379, db=0)
這段代碼創(chuàng)建了一個Redis數(shù)據(jù)連接。在Redis中,數(shù)據(jù)由整數(shù)索引區(qū)分,而不是數(shù)據(jù)庫名。默認情況下,客戶端連接到數(shù)據(jù)庫0。Redis數(shù)據(jù)庫有效的數(shù)字到16,但你可以在redis.conf文件中修改這個值。
現(xiàn)在使用Python終端設置一個鍵:
>>> r.set('foo', 'bar')
True
命令返回True表示鍵創(chuàng)建成功?,F(xiàn)在你可以使用get()命令查詢鍵:
>>> r.get('foo')
b'bar'
正如你鎖看到的,StrictRedis方法遵循Redis命令語法。
讓我們在項目中集成Redis。編輯bookmarks項目的settings.py文件,添加以下設置:
REDIS_HOST = 'localhost'
REDIS_PORT = 6379
REDIS_DB = 0
以上設置了Redis服務器和我們在項目中使用的數(shù)據(jù)庫。
6.4.3 在Redis中存儲項的瀏覽次數(shù)
讓我們存儲一張圖片被查看的總次數(shù)。如果我們使用Django ORM,則每次顯示圖片后,都會涉及UPDATE語句。如果使用Redis,我們只需要增加內(nèi)存中的計數(shù),從而獲得更好的性能。
編輯images應用的views.py文件,添加以下代碼:
import redis
from django.conf import settings
# connect to redis
r = redis.StrictRedis(host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
db=settings.REDIS_DB)
我們建立了Redis連接,以便在視圖中使用。修改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})
在這個視圖中,我們使用INCR命令把一個鍵的值加1,如果鍵不存在,則在執(zhí)行操作之前設置值為0。incr()方法返回執(zhí)行操作之后鍵的值,我們把它存在total_views變量中。我們用object-type:id:field(比如image:33:id:views)構建Redis鍵。
Redis鍵的命名慣例是使用冒號分割,來創(chuàng)建帶命名空間的鍵。這樣鍵名會很詳細,并且相關的鍵共享部分相同的模式。
編輯image/detail.html模板,在<span class="count">元素之后添加以下代碼:
<span class="count">
<span class="total">{{ total_views }}</span>
view{{ total_views|pluralize }}
</span>
現(xiàn)在在瀏覽器中打開圖片詳情頁面,加載多次。你會看到每次執(zhí)行視圖,顯示的瀏覽總數(shù)都會加1,如下圖所示:

你已經(jīng)成功的在項目集成了Redis,來存儲項的瀏覽次數(shù)。
6.4.4 在Redis中存儲排名
讓我們用Redis構建更多功能。我們將創(chuàng)建瀏覽次數(shù)最多的圖片排名。我們將使用Redis的sorted set來構建排名。一個sorted set是一個不重復的字符串集合,每個成員關聯(lián)一個分數(shù)。項通過它們的分數(shù)存儲。
編輯images應用的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中存儲圖片瀏覽次數(shù),其中鍵為image_ranking。我們存儲圖片id,分數(shù)1會被加到sorted set中這個元素的總分上。這樣就可以全局追蹤所有圖片的瀏覽次數(shù),并且有一個按瀏覽次數(shù)排序的sorted set。
現(xiàn)在創(chuàng)建一個新視圖,用于顯示瀏覽次數(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中的元素。這個命令通過最低和最高分指定自定義范圍。通過0作為最低,-1作為最高分,我們告訴Redis返回sorted set中的所有元素。我們還指定desc=True,按分數(shù)的降序排列返回元素。最后,我們用[:10]切片操作返回分數(shù)最高的前10個元素。我們構建了一個返回的圖片ID列表,并作為整數(shù)列表存在image_ranking_ids變量中。我們迭代這些ID的Image對象,并使用list()函數(shù)強制執(zhí)行查詢。強制QuerySet執(zhí)行很重要,因為之后我們要調(diào)用列表的sort()方法(此時我們需要一組對象,而不是一個QuerySet)。我們通過Image對象在圖片排名中的索引進行排序?,F(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 %}
這個模板非常簡單,我們迭代most_viewed列表中的Image對象。
最后為新視圖創(chuàng)建URL模式。編輯images應用的urls.py文件,添加以下模式:
url(r'^/ranking/$', views.image_ranking, name='ranking')
在瀏覽器中打開http://127.0.0.1:8000/images/ranking/,你會看到圖片排名,如下圖所示:

6.4.5 Redis的后續(xù)功能
Redis不是SQL數(shù)據(jù)庫的替代者,而是更適用于特定任務的快速的內(nèi)存存儲。當你真的需要時可以使用它。Redis非常適合以下場景:
- 計數(shù):正如你所看到的,使用Redis管理計算非常簡單。你可以使用
incr()和incrby()計數(shù)。 - 存儲最近的項:你可以使用
lpush()和rpush()在列表開頭或結尾添加項。使用lpop()或rpop()移除并返回第一或最后一項。你可以使用ltrim()截斷列表長度。 - 隊列:除了
push和pop命令,Redis還提供了阻塞隊列的命令。 - 緩存:使用
expire()和expireat()允許你把Redis當做緩存。你還可以找到Django的第三方Redis緩存后臺。 - 訂閱/發(fā)布:Redis還為訂閱/取消訂閱,以及發(fā)送消息給頻道提供了命令。
- 排名和排行榜:Redis的
sorted set可以很容易創(chuàng)建排行榜。 - 實時跟蹤:Redis的快速I/O非常適合實時場景。
6.5 總結
這一章中,你構建了關注系統(tǒng)和用戶活動流。你學習了Django信號是如何工作的,并在項目中集成了Redis。
下一章中,你會學習如何構建一個在線商店。你將創(chuàng)建一個產(chǎn)品目錄,并使用會話構建購物車。你還講學習如何使用Celery啟動異步任務。