第六章 跟蹤用戶動(dòng)作

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=TrueDateTimeField字段,存儲(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_setrel_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-patchUser模型中。不推薦使用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-patchUser模型。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 %}
                        ![]({{ im.url }})
                    {% endthumbnail %}
                </a>
                <div class="info">
                    <a href="{{ user.get_absolute_url }}" class="title">
                        {{ user.get_full_name }}
                    </a>
                </div>
            </div>
        {% endfor %}
    </div>
{% endblock %}

該模板列出網(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 %}
            ![]({{ im.url }})
        {% endthumbnail %}
    </div>
    {% with total_followers=user.followers.count %}
        <span class="count">
            <span class="total">{{ total_followers }}</span>
            follower{{ total_followers|pluralize }}
        </span>
        <a href="#" data-id="{{ user.id }}" 
            data-action="{% if request.user in user.followers.all %}un{% endif %}follow" 
            class="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-iddata-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_namemodel屬性的查詢,你可以獲得指定模型對應(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_cttarget_id字段會(huì)映射到數(shù)據(jù)庫的字段。因?yàn)檫@兩個(gè)字段都有blank=Truenull=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_cttarget_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方法用于ForeignKeyOneToOne字段。它在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_relatedQuerySet方法,除了select_related()支持的關(guān)系之外,還可以用于多對多和多對一關(guān)系。prefetch_related()方法為每個(gè)關(guān)系執(zhí)行獨(dú)立的查詢,然后用Python連接結(jié)果。該方法還支持GenericRelationGenericForeignKey的預(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 }}">
                        ![]({{ im.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 }}">
                                ![]({{ im.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_savepost_save:調(diào)用模型的save()方法之前或之后發(fā)送
  • pre_deletepost_delete:調(diào)用模型或QuerySetdelete()方法之前或之后發(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),比如stringshasheslistssetsordered sets,甚至bitmapsHyperLogLogs

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è)字符串值為Petername鍵。輸出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ù)類型(比如stringshasheslistssetsordered sets等等)提供了大量的命令。你可以在這里查看所有Redis命令,在這里查看所有Redis數(shù)據(jù)類型。

6.4.2 在Python中使用Redis

我們需要為Redis綁定Python。通過pip安裝redis-py

pip install redis

你可以在這里查看redis-py的文檔。

redis-py提供了兩個(gè)類用于與Redis交互:StricRedisRedis。兩個(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ì)列:除了pushpop命令,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ù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容