《Django By Example》第十一章 中文 翻譯 (個人學習,渣翻)

全文鏈接

第一章 創建一個blog應用
第二章 使用高級特性來增強你的blog
第三章 擴展你的blog應用
第四章上 創建一個社交網站
第四章下 創建一個社交網站
第五章 在你的網站中分享內容
第六章 跟蹤用戶動作
第七章 建立一個在線商店
第八章 管理付款和訂單
第九章上 擴展你的商店
第九章下 擴展你的商店
第十章上 創建一個在線學習平臺
第十章下 創建一個在線學習平臺
第十一章 緩存內容
第十二章 構建一個API

第十一章

緩存內容

(譯者 @ucag 注:這是倒數第二章,最后一個項目即將完成。 @夜夜月 將會接過翻譯的最后一棒。這本書的翻譯即將完成。這也是我翻譯的最后一章,作為英語專業的學生,我對于翻譯有了更多的思考。同樣,校對是 @夜夜月)

(譯者 @夜夜月注:贊,倒數第二章了,接下來就是最后一章了!)

在上一章中,你使用模型繼承和一般關系創建了一個靈活的課程內容模型。你也使用基于類的視圖,表單集,以及 內容的 AJAX 排序,創建了一個課程管理系統。在這一章中,你將會:

  • 創建展示課程信息的公共視圖
  • 創建一個學生注冊系統
  • courses中管理學生注冊
  • 創建多樣化的課程內容
  • 使用緩存框架緩存內容

我們將會從創建一個課程目錄開始,好讓學生能夠瀏覽當前的課程以及注冊這些課程。

展示課程

對于課程目錄,我們需要創建以下的功能:

  • 列出所有的可用課程,可以通過可選科目過濾
  • 展示一個單獨的課程概覽

編輯 courses 應用的 views.py ,添加以下代碼:

from django.db.models import Count
from .models import Subject

class CourseListView(TemplateResponseMixin, View):
    model = Course
    template_name = 'courses/course/list.html'
    def get(self, request, subject=None):
        subjects = Subject.objects.annotate(
                      total_courses=Count('courses'))
        courses = Course.objects.annotate(
                      total_modules=Count('modules'))
        if subject:
            subject = get_object_or_404(Subject, slug=subject)
            courses = courses.filter(subject=subject)
       return self.render_to_response({'subjects':subjects,
                                               'subject': subject,
                                               'courses': courses})

這是 CourseListView 。它繼承了 TemplateResponseMixinView 。在這個視圖中,我們實現了下面的功能:

  • 1 我們檢索所有的課程,包括它們當中的每個課程總數。我們使用 ORM 的 annotate() 方法和 Count() 聚合方法來實現這一功能。
  • 2 我們檢索所有的可用課程,包括在每個課程中包含的模塊總數。
  • 3 如果給了科目的 slug URL 參數,我們就檢索對應的課程對象,然后我們將會把查詢限制在所給的科目之內。
  • 4 我們使用 TemplateResponseMixin 提供的 render_to_response() 方法來把對象渲染到模板中,然后返回一個 HTTP 響應。

讓我們創建一個詳情視圖來展示單一課程的概覽。在 views.py 中添加以下代碼:

from django.views.generic.detail import DetailView

class CourseDetailView(DetailView):
    model = Course
    template_name = 'courses/course/detail.html'

這個視圖繼承了 Django 提供的通用視圖 DetailView 。我們定義了 modeltemplate_name 屬性。Django 的 DetailView 期望一個 主鍵(pk) 或者 slug URL 參數來檢索對應模型的一個單一對象。然后它就會渲染 template_name 中的模板,同樣也會把上下文中的對象渲染進去。

編輯 educa 項目中的主 urls.py 文件,添加以下 URL 模式:

from courses.views import CourseListView

urlpatterns = [
    # ...
    url(r'^$', CourseListView.as_view(), name='course_list'),
]

我們把 course_list 的 URL 模式添加進了項目的主 urls.py 中,因為我們想要把課程列表展示在 http://127.0.0.1:8000/ 中,然后 courses 應用的所有的其他 URL 都有 /course/ 前綴。

編輯 courses 應用的 urls.py ,添加以下 URL 模式:

url(r'^subject/(?P<subject>[\w-]+)/$',
    views.CourseListView.as_view(),
    name='course_list_subject'),

url(r'^(?P<slug>[\w-]+)/$',
    views.CourseDetailView.as_view(),
    name='course_detail'),

我們定義了以下 URL 模式:

  • course_list_subject:用于展示一個科目的所有課程
  • course_detail:用于展示一個課程的概覽

讓我們為 CourseListViewCourseDetailView 創建模板。在 courses 應用的 templates/courses/ 路徑下創建以下路徑:

  • course/
  • list.html
  • detail.html

編輯 courses/course/list.html 模板,寫入以下代碼:

{% extends "base.html" %}

{% block title %}
    {% if subject %}
        {{ subject.title }} courses
    {% else %}
        All courses
    {% endif %}
{% endblock %}

{% block content %}
<h1>
    {% if subject %}
        {{ subject.title }} courses
    {% else %}
        All courses
    {% endif %}
</h1>
<div class="contents">
    <h3>Subjects</h3>
    <ul id="modules">
        <li {% if not subject %}class="selected"{% endif %}>
            <a href="{% url "course_list" %}">All</a>
        </li>
        {% for s in subjects %}
            <li {% if subject == s %}class="selected"{% endif %}>
                <a href="{% url "course_list_subject" s.slug %}">
                    {{ s.title }}
                    <br><span>{{ s.total_courses }} courses</span>
                </a>
            </li>
        {% endfor %}
    </ul>
</div>
<div class="module">
    {% for course in courses %}
        {% with subject=course.subject %}
            <h3><a href="{% url "course_detail" course.slug %}">{{ course.title }}</a></h3>
            <p>
                <a href="{% url "course_list_subject" subject.slug %}">{{ subject }}</a>.
                {{ course.total_modules }} modules.
                Instructor: {{ course.owner.get_full_name }}
            </p>
        {% endwith %}
    {% endfor %}
</div>
{% endblock %}

這個模板用于展示可用課程列表。我們創建了一個 HTML 列表來展示所有的 Subject 對象,然后為它們每一個都創建了一個鏈接,這個鏈接鏈接到 course_list_subject 的 URL 。 如果存在當前科目,我們就把 selected HTML 類添加到當前科目中高亮顯示該科目。我們迭代每個 Course 對象,展示模塊的總數和教師的名字。

使用 python manage.py runserver 打開代發服務器,訪問 http://127.0.0.1:8000/ 。你看到的應該是像下面這個樣子:

django-11-1

左邊的側邊欄包含了所有的科目,包括每個科目的課程總數。你可以點擊任意一個科目來篩選展示的課程。

編輯 courses/course/detail.html 模板,添加以下代碼:

{% extends "base.html" %}

{% block title %}
    {{ object.title }}
{% endblock %}

{% block content %}
    {% with subject=course.subject %}
        <h1>
            {{ object.title }}
        </h1>
        <div class="module">
            <h2>Overview</h2>
            <p>
                <a href="{% url "course_list_subject" subject.slug %}">{{ subject.title }}</a>.
                {{ course.modules.count }} modules.
                Instructor: {{ course.owner.get_full_name }}
            </p>
            {{ object.overview|linebreaks }}
        </div>
    {% endwith %}
{% endblock %}

在這個模板中,我們展示了每個單一課程的概覽和詳情。訪問 http://127.0.0.1:8000/ ,點擊任意一個課程。你就應該看到有下面結構的頁面了:

django-11-2

我們已經創建了一個展示課程的公共區域了。下面,我們需要讓用戶可以注冊為學生以及注冊他們的課程。

添加學生注冊

使用下面的命令創鍵一個新的應用:

python manage.py startapp students

編輯 educa 項目的 settings.py ,把 students 添加進 INSTALLED_APPS 設置中:

INSTALLED_APPS = (
    # ...
    'students',
)

創建一個學生注冊視圖

編輯 students 應用的 views.py ,寫入下下面的代碼:

from django.core.urlresolvers import reverse_lazy
from django.views.generic.edit import CreateView
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import authenticate, login

class StudentRegistrationView(CreateView):
    template_name = 'students/student/registration.html'
    form_class = UserCreationForm
    success_url = reverse_lazy('student_course_list')
    
    def form_valid(self, form):
        result = super(StudentRegistrationView,
                        self).form_valid(form)
        cd = form.cleaned_data
        user = authenticate(username=cd['username'],
                            password=cd['password1'])
        login(self.request, user)
        return result

這個視圖讓學生可以注冊進我們的網站里。我們使用了可以提供創建模型對象功能的通用視圖 CreateView 。這個視圖要求以下屬性:

  • template_name:渲染這個視圖的模板路徑。
  • form_class:用于創建對象的表單,我們使用 Django 的 UserCreationForm 作為注冊表單來創建 User 對象。
  • success_url:當表單成功提交時要將用戶重定向到的 URL 。我們逆序了 student_course_list URL,我們稍候將會將建它來展示學生已報名的課程。

當合法的表單數據被提交時 form_valid() 方法就會執行。它必須返回一個 HTTP 響應。我們覆寫了這個方法來讓用戶在成功注冊之后登錄。

在 students 應用路徑下創建一個新的文件,命名為 urls.py ,添加以下代碼:

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^register/$',
           views.StudentRegistrationView.as_view(),
           name='student_registration'),
]

編輯 educa 的主 urls.py ,然后把 students 應用的 URLs 引入進去:

url(r'^students/', include('students.urls')),

students 應用內創建如下的文件結構:

templates/
    students/
        student/
            registration.html

編輯 students/student/registration.html 模板,然后添加以下代碼:

{% extends "base.html" %}

{% block title %}
    Sign up
{% endblock %}

{% block content %}
    <h1>
        Sign up
    </h1>
    <div class="module">
        <p>Enter your details to create an account:</p>
        <form action="" method="post">
            {{ form.as_p }}
            {% csrf_token %}
            <p><input type="submit" value="Create my account"></p>
        </form>
    </div>
{% endblock %}

最后編輯 educa 的設置文件,添加以下代碼:

from django.core.urlresolvers import reverse_lazy
LOGIN_REDIRECT_URL = reverse_lazy('student_course_list')

這個是由 auth 模型用來給用戶在成功的登錄之后重定向的設置,如果請求中沒有 next 參數的話。

打開開發服務器,訪問 http://127.0.0.1:8000/students/register/ ,你可以看到像這樣的注冊表單:

django-11-3

報名

在用戶創建一個賬號之后,他們應該就可以在 courses 中報名了。為了保存報名表,我們需要在 CourseUser 模型之間創建一個多對多關系。編輯 courses 應用的 models.py 然后把下面的字段添加進 Course 模型中:

students = models.ManyToManyField(User,
                        related_name='courses_joined',
                        blank=True)

在 shell 中執行下面的命令來創建遷移:

python manage.py makemigrations

你可以看到類似下面的輸出:

Migrations for 'courses':
    0004_course_students.py:
       - Add field students to course

接下來執行下面的命令來應用遷移:

python manage.py migrate

你可以看到以下輸出:

Operations to perform:
    Apply all migrations: courses
Running migrations:
    Rendering model states... DONE
    Applying courses.0004_course_students... OK

我們現在就可以把學生和他們報名的課程相關聯起來了。
讓我們創建學生報名課程的功能吧。

students 應用內創建一個新的文件,命名為 forms.py ,添加以下代碼:

from django import forms
from courses.models import Course

class CourseEnrollForm(forms.Form):
    course = forms.ModelChoiceField(queryset=Course.objects.all(),
                                    widget=forms.HiddenInput)

我們將會把這張表用于學生報名。course 字段是學生報名的課程。所以,它是一個 ModelChoiceField 。我們使用 HiddenInput 控件,因為我們不打算把這個字段展示給用戶。我們將會在 CourseDetailView 視圖中使用這個表單來展示一個報名按鈕。

編輯 students 應用的 views.py ,添加以下代碼:

from django.views.generic.edit import FormView
from braces.views import LoginRequiredMixin
from .forms import CourseEnrollForm

class StudentEnrollCourseView(LoginRequiredMixin, FormView):
    course = None
    form_class = CourseEnrollForm
    
    def form_valid(self, form):
        self.course = form.cleaned_data['course']
        self.course.students.add(self.request.user)
        return super(StudentEnrollCourseView,
                        self).form_valid(form)

    def get_success_url(self):
        return reverse_lazy('student_course_detail',
                                args=[self.course.id])

這就是 StudentEnrollCourseView 。它負責學生在 courses 中報名。新的視圖繼承了 LoginRequiredMixin ,所以只有登錄了的用戶才可以訪問到這個視圖。我們把 CourseEnrollForm表單用在了 form_class 屬性上,同時我們也定義了一個 course 屬性來儲存所給的 Course 對象。當表單合法時,我們把當前用戶添加到課程中已報名學生中去。

如果表單提交成功,get_success_url 方法就會返回用戶將會被重定向到的 URL 。這個方法相當于 success_url 屬性。我們反序 student_course_detail URL ,我們稍候將會創建它來展示課程中的學生。

編輯 students 應用的 urls.py ,添加以下 URL 模式:

url(r'^enroll-course/$',
    views.StudentEnrollCourseView.as_view(),
    name='student_enroll_course'),

讓我們把報名按鈕表添加進課程概覽頁。編輯 course 應用的 views.py ,然后修改 CourseDetailView 讓它看起來像這樣:

from students.forms import CourseEnrollForm

class CourseDetailView(DetailView):
    model = Course
    template_name = 'courses/course/detail.html'
    
    def get_context_data(self, **kwargs):
        context = super(CourseDetailView,
                        self).get_context_data(**kwargs)
        context['enroll_form'] = CourseEnrollForm(
                        initial={'course':self.object})
        return context

我們使用 get_context_data() 方法來在渲染進模板中的上下文里引入報名表。我們初始化帶有當前 Course 對象的表單的隱藏 course 字段,這樣它就可以被直接提交了。

編輯 courses/course/detail.html 模板,然后找到下面的這一行:

{{ object.overview|linebreaks }}

起始行應該被替換為下面的這幾行:

{{ object.overview|linebreaks }}
{% if request.user.is_authenticated %}
    <form action="{% url "student_enroll_course" %}" method="post">
        {{ enroll_form }}
        {% csrf_token %}
        <input type="submit" class="button" value="Enroll now">
    </form>
{% else %}
    <a href="{% url "student_registration" %}" class="button">
        Register to enroll
    </a>
{% endif %}

這個按鈕就是用于報名的。如果用戶是被認證過的,我們就展示包含了隱藏表單字段的報名按鈕,這個表單指向了 student_enroll_course URL。如果用戶沒有被認證,我們將會展示一個注冊鏈接。

確保已經打開了開發服務器,訪問 http://127.0.0.1:8000/ ,然后點擊一個課程。如果你登錄了,你就可以在底部看到 ENROLL NOW 按鈕,就像這樣:

django-11-4

如果你沒有登錄,你就會看到一個Register to enroll 的按鈕。

獲取課程內容

我們需要一個視圖來展示學生已經報名的課程,和一個獲取當前課程內容的視圖。編輯 students 應用的 views.py ,添加以下代碼:

from django.views.generic.list import ListView
from courses.models import Course

class StudentCourseListView(LoginRequiredMixin, ListView):
    model = Course
    template_name = 'students/course/list.html'
    
    def get_queryset(self):
        qs = super(StudentCourseListView, self).get_queryset()
        return qs.filter(students__in=[self.request.user])

這個是用于列出學生已經報名課程的視圖。它繼承 LoginRequiredMixin 來確保只有登錄的用戶才可以連接到這個視圖。同時它也繼承了通用視圖 ListView 來展示 Course 對象列表。我們覆寫了 get_queryset() 方法來檢索用戶已經報名的課程。我們通過學生的 ManyToManyField 字段來篩選查詢集以達到這個目的。

把下面的代碼添加進 views.py 文件中:

from django.views.generic.detail import DetailView

class StudentCourseDetailView(DetailView):
    model = Course
    template_name = 'students/course/detail.html'
    
    def get_queryset(self):
        qs = super(StudentCourseDetailView, self).get_queryset()
        return qs.filter(students__in=[self.request.user])
    
    def get_context_data(self, **kwargs):
        context = super(StudentCourseDetailView,
                        self).get_context_data(**kwargs)
        # get course object
        course = self.get_object()
        if 'module_id' in self.kwargs:
            # get current module
            context['module'] = course.modules.get(
                            id=self.kwargs['module_id'])
        else:
            # get first module
            context['module'] = course.modules.all()[0]
        return context

這是 StudentCourseDetailView 。我們覆寫了 get_queryset 方法把查詢集限制在用戶報名的課程之內。我們同樣也覆寫了 get_context_data() 方法來把課程的一個模塊賦值在上下文內,如果給了 model_id URL 參數的話。否則,我們就賦值課程的第一個模塊。這樣,學生就可以在課程之內瀏覽各個模塊了。

編輯 students 應用的 urls.py ,添加以下 URL 模式:

url(r'^courses/$',
    views.StudentCourseListView.as_view(),
    name='student_course_list'),

url(r'^course/(?P<pk>\d+)/$',
    views.StudentCourseDetailView.as_view(),
    name='student_course_detail'),

url(r'^course/(?P<pk>\d+)/(?P<module_id>\d+)/$',
    views.StudentCourseDetailView.as_view(),
    name='student_course_detail_module'),

students 應用的 templates/students/ 路徑下創建以下文件結構:

course/
    detail.html
    list.html

編輯 students/course/list.html 模板,然后添加下列代碼:

{% extends "base.html" %}

{% block title %}My courses{% endblock %}

{% block content %}
    <h1>My courses</h1>

    <div class="module">
        {% for course in object_list %}
            <div class="course-info">
                <h3>{{ course.title }}</h3>
                <p><a href="{% url "student_course_detail" course.id %}">Access contents</a></p>
            </div>
        {% empty %}
            <p>
                You are not enrolled in any courses yet.
                <a href="{% url "course_list" %}">Browse courses</a>to enroll in a course.
            </p>
        {% endfor %}
    </div>
{% endblock %}

這個模板展示了用戶報名的課程。編輯 students/course/detail.html 模板,添加以下代碼:

{% extends "base.html" %}

{% block title %}
    {{ object.title }}
{% endblock %}

{% block content %}
    <h1>
        {{ module.title }}
    </h1>
    <div class="contents">
        <h3>Modules</h3>
        <ul id="modules">
        {% for m in object.modules.all %}
            <li data-id="{{ m.id }}" {% if m == module %}
class="selected"{% endif %}>
                <a href="{% url "student_course_detail_module" object.id m.id %}">
                    <span>
                        Module <span class="order">{{ m.order|add:1 }}</span>
                    </span>
                    <br>
                    {{ m.title }}
                </a>
            </li>
        {% empty %}
            <li>No modules yet.</li>
        {% endfor %}
        </ul>
    </div>
    <div class="module">
        {% for content in module.contents.all %}
            {% with item=content.item %}
                <h2>{{ item.title }}</h2>
                {{ item.render }}
            {% endwith %}
        {% endfor %}
    </div>
{% endblock %}

這個模板用于報名了的學生連接到課程內容。首先我們創建了一個包含所有課程模塊的 HTML 列表且高亮當前模塊。然后我們迭代當前的模塊內容,之后使用 {{ item.render }} 來連接展示內容。接下來我們將會在內容模型中添加 render() 方法。這個方法將會負責精準的展示內容。

渲染不同類型的內容

我們需要提供一個方法來渲染不同類型的內容。編輯 course 應用的 models.py ,把 render() 方法添加進 ItemBase 模型中:

from django.template.loader import render_to_string
from django.utils.safestring import mark_safe

class ItemBase(models.Model):
    # ...
    def render(self):
        return render_to_string('courses/content/{}.html'.format(
                            self._meta.model_name), {'item': self})

這個方法使用了 render_to_string() 方法來渲染模板以及返回一個作為字符串的渲染內容。每種內容都使用以內容模型命名的模板渲染。我們使用 self._meta.model_name 來為 la 創建合適的模板名。 render() 方法提供了一個渲染不同頁面的通用接口。

courses 應用的 templates/courses/ 路徑下創建如下文件結構:

content/
    text.html
    file.html
    image.html
    video.html

編輯 courses/content/text.html 模板,寫入以下代碼:

{{ item.content|linebreaks|safe }}

編輯 courses/content/file.html 模板,寫入以下代碼:

<p><a href="{{ item.file.url }}" class="button">Download file</a></p>

編輯 courses/content/image.html 模板,寫入以下代碼:

<p>![]({{ item.file.url }})</p>

為了使上傳帶有 ImageFieldFielField 的文件工作,我們需要配置我們的項目以使用開發服務器提供媒體文件服務。編輯你的項目中的 settings.py ,添加以下代碼:

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')

記住 MEDIA_URL 是服務上傳文件的基本 URL 路徑, MEDIA_ROOT 是放置文件的本地路徑。

編輯你的項目的主 urls.py ,添加以下 imports:

from django.conf import settings
from django.conf.urls.static import static

然后,把下面這幾行寫入文件的結尾:

urlpatterns += static(settings.MEDIA_URL,
                document_root=settings.MEDIA_ROOT)

你的項目現在已經準備好使用開發服務器上傳和服務文件了。記住開發服務器不能用于生產環境中。我們將會在下一章中學習如何配置生產環境。

我們也需要創建一個模板來渲染 Video 對象。我們將會使用 django-embed-video 來嵌入視頻內容。 Django-embed-video 是一個第三方 Django 應用,它使你可以通過提供一個視頻的公共 URL 來在模板中嵌入視頻,類似來自 YouTube 或者 Vimeo 的資源。

使用下面的命令來安裝這個包:

pip isntall django-embed-video==1.0.0

然后編輯項目的 settings.py 然后添加 embed_videoINSTALLED_APPS設置 中。你可以在這里找到 django-embed-video 的文檔:
http://django-embed-video.readthedocs.org/en/v1.0.0/

編輯 courses/content/video.html 模板,寫入以下代碼:

{% load embed_video_tags %}
{% video item.url 'small' %}

現在運行開發服務器,訪問 http://127.0.0.1:8000/course/mine/ 。用屬于教師組或者超級管理員的用戶訪問站點,然后添加一些內容到一個課程中。為了引入視頻內容,你也可以復制任何一個 YouTube 視頻 URL ,比如 :https://www.youtube.com/watch?n=bgV39DlmZ2U ,然后把它引入到表單的 url 字段中。在添加內容到課程中之后,訪問 http://127.0.0.1:8000/ ,點擊課程然后點擊ENROLL NOW按鈕。你就可以在課程中報名了,然后被重定向到 student_course_detail URL 。下面這張圖片展示了一個課程內容樣本:

django-11-5

真棒!你已經創建了一個渲染課程的通用接口了,它們中的每一個都會被用特定的方式渲染。

使用緩存框架

你的應用的 HTTP 請求通常是數據庫鏈接,數據處理,和模板渲染的。就處理數據而言,它的開銷可比服務一個靜態網站大多了。

請求開銷在你的網站有越來越多的流量時是有意義的。這也使得緩存變得很有必要。通過緩存 HTTP 請求中 的查詢結果,計算結果,或者是渲染上下文,你將會避免在接下來的請求中巨大的開銷。這使得服務端的響應時間和處理時間變短。

Django 配備有一個健碩的緩存系統,這使得你可以使用不同級別的顆粒度來緩存數據。你可以緩存單一的查詢,一個特定的輸出視圖,部分渲染的模板上下文,或者整個網站。緩存系統中的內容會在默認時間內被儲存。你可以指定緩存數據過期的時間。

這是當你的站點收到一個 HTTP 請求時將會通常使用的緩存框架的方法:

1. 試著在緩存中尋找緩存數據
2. 如果找到了,就返回緩存數據
3. 如果沒有找到,就執行下面的步驟:
    1. 執行查詢或者處理請求來獲得數據
    2. 在緩存中保存生成的數據
    3. 返回數據

你可以在這里找到更多關于 Django 緩存系統的細節信息:https://docs.djangoproject.com/en/1.8/topics/cache/

激活緩存后端

Django 配備有幾個緩存后端,他們是:

  • backends.memcached.MemcachedCachebackends.memcached.PyLibMCCache:一個內存緩存后端。內存緩存是一個快速、高效的基于內存的緩存服務器。后端的使用取決于你選擇的 Python 綁定(bindings)。
  • backends.db.DatabaseCache: 使用數據庫作為緩存系統。
  • backends.filebased.FileBasedCache:使用文件儲存系統。把每個緩存值序列化和儲存為單一的文件。
  • backends.locmem.LocMemCache:本地內存緩存后端。這是默認的緩存后端
  • backends.dummy.DummyCache:一個用于開發的虛擬緩存后端。它實現了緩存交互界面而不用真正的緩存任何東西。緩存是獨立進程且是線程安全的

對于可選的實現,使用內存的緩存后端吧,比如 Memcached 后端。

安裝 Memcached

我們將會使用 Memcached 緩存后端。內存緩存運行在內存中,它在 RAM 中分配了指定的數量。當分配的 RAM 滿了時,Memcahed 就會移除最老的數據來保存新的數據。

在這個網址下載 Memcached: http://memcached.org/downloads 。如果你使用 Linux, 你可以使用下面的命令安裝:

./configure && make && make test && sudo make install

如果你在使用 Mac OS X, 你可以使用命令 brew install Memcached 通過 Homebrew 包管理器來安裝 Memcached 。你可以在這里下載 Homebrew http://brew.sh

如果你正在使用 Windwos ,你可以在這里找到一個 Windows 的 Memcached 二進制版本:http://code.jellycan.com/memcached/

在安裝 Memcached 之后,打開 shell ,使用下面的命令運行它:

memcached -l 127.0.0.1:11211

Memcached 將會默認地在 11211 運行。當然,你也可以通過 -l 選項指定一個特定的主機和端口。你可以在這里找到更多關于 Memcached 的信息:http://memcached.org

在安裝 Memcached 之后,你需要安裝它的 Python 綁定(bindings)。使用下面的命令安裝:】

python install python3-memcached==1.51

緩存設置

Django 提供了如下的緩存設置:

  • CACHES:一個包含所有可用的項目緩存。
  • CACHE_MIDDLEWARE_ALIAS:用于儲存的緩存別名。
  • CACHE_MIDDLEWARE_KEY_PREFIX:緩存鍵的前綴。設置一個緩存前綴來避免鍵的沖突,如果你在幾個站點中分享相同的緩存的話。
  • CACHE_MIDDLEWARE_SECONDS :默認的緩存頁面秒數

項目的緩存系統可以使用 CACHES 設置來配置。這個設置是一個字典,讓你可以指定多個緩存的配置。每個 CACHES 字典中的緩存可以指定下列數據:

  • BACKEND:使用的緩存后端。
  • KEY_FUNCTION:包含一個指向回調函數的點路徑的字符,這個函數以prefix(前綴)、verision(版本)、和 key (鍵) 作為參數并返回最終緩存鍵(cache key)。
  • KEY_PREFIX:一個用于所有緩存鍵的字符串,避免沖突。
  • LOCATION:緩存的位置。基于你的緩存后端,這可能是一個路徑、一個主機和端口,或者是內存中后端的名字。
  • OPTIONS:任何額外的傳遞向緩存后端的參數。
  • TIMEOUT:默認的超時時間,以秒為單位,用于儲存緩存鍵。默認設置是 300 秒,也就是五分鐘。如果把它設置為 None ,緩存鍵將不會過期。
  • VERSION:默認的緩存鍵的版本。對于緩存版本是很有用的。

把 memcached 添加進你的項目

讓我們為我們的項目配置緩存。編輯 educa 項目的 settings.py 文件,添加以下代碼:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.
MemcachedCache',
    'LOCATION': '127.0.0.1:11211',
    }
}

我們正在使用 MemcachedCache 后端。我們使用 address:port 標記指定了它的位置。如果你有多個 memcached 實例,你可以在 LOCATION 中使用列表。

監控緩存

這里有一個第三方包叫做 django-memcached-status ,它可以在管理站點展示你的項目的 memcached 實例的統計數據。為了兼容 Python3(譯者夜夜月注:python3大法好。) ,從下面的分支中安裝它:

pip install git+git://github.com/zenx/django-memcached-status.git

編輯 settings.py ,然后把 memcached_status 添加進 INSTALLED_APPS 設置中。確保 memcached 正在運行,在另外一個 shell 中打開開發服務器,然后訪問 http://127.0.0.1:8000/adim/ ,使用超級用戶登錄進管理站點,你就可以看到如下的區域:

django-11-6

這張圖片展示了緩存使用。綠色代表了空閑的緩存,紅色的表示使用了的空間。如果你點擊方框的標題,它展示了你的 memcached 實例的統計詳情。

我們已經為項目安裝好了 memcached 并且可以監控它。讓我們開始緩存數據吧!

緩存級別

Django 提供了以下幾個級別按照顆粒度上升的緩存排列:

  • Low-level cache API:提供了最高顆粒度。允許你緩存具體的查詢或計算結果。
  • Per-view cache:提供單一視圖的緩存。
  • Template cache:允許你緩存模板片段。
  • Per-site cache:最高級的緩存。它緩存你的整個網站。

在你執行緩存之請仔細考慮下緩存策略。首先要考慮那些不是單一用戶為基礎的查詢和計算開銷

使用 low-level cache API (低級緩存API)

低級緩存 API 讓你可以緩存任意顆粒度的對象。它位于 django.core.cache 。你可以像這樣導入它:

from django.core.cache import cache

這使用的是默認的緩存。它相當于 caches['default'] 。通過它的別名來連接一個特定的緩存也是可能的:

from django.core.cache import caches
my_cache = caches['alias']

讓我們看看緩存 API 是如何工作的。使用命令 python manage.py shell 打開 shell 然后執行下面的代碼:

>>> from django.core.cache import cache
>>> cache.set('musician', 'Django Reinhardt', 20)

我們連接的是默認的緩存后端,使用 set{key,value, timeout} 來保存一個名為 musician 的鍵和它的為字符串 Django Reinhardt 的值 20 秒鐘。如果我們不指定過期時間,Django 會使在 CACHES 設置中緩存后端的默認過期時間。現在執行下面的代碼:

>>> cache.get('musician')
'Django Reinhardt'

我們在緩存中檢索鍵。等待 20 秒然后指定相同的代碼:

>>> cache.get('musician')
None

musician 緩存鍵已經過期了,get() 方法返回了 None 因為鍵已經不在緩存中了。

在緩存鍵中要避免儲存 None 值,因為這樣你就無法區分緩存值和緩存過期了

讓我們緩存一個查詢集:

>>> from courses.models import Subject
>>> subjects = Subject.objects.all()
>>> cache.set('all_subjects', subjects)

我們執行了一個在 Subject 模型上的查詢集,然后把返回的對象儲存在 all_subjects 鍵中。讓我們檢索一下緩存數據:

>>> cache.get('all_subjects')
[<Subject: Mathematics>, <Subject: Music>, <Subject: Physics>,
<Subject: Programming>]

我們將會在視圖中緩存一些查詢集。編輯 courses 應用的 views.py ,添加以下 導入:

from django.core.cache import cache

CourseListViewget() 方法,把下面這幾行:

subjects = Subject.objects.annotate(
        total_courses=Count('courses'))

替換為:

subjects = cache.get('all_subjects')
if not subjects:
    subjects = Subject.objects.annotate(
                total_courses=Count('courses'))
    cache.set('all_subjects', subjects)

在這段代碼中,我們首先嘗試使用 cache.get() 來從緩存中得到 all_students 鍵。如果所給的鍵沒有找到,返回的是 None 。如果鍵沒有被找到(沒有被緩存,或者緩存了但是過期了),我們就執行查詢來檢索所有的 Subject 對象和它們課程的數量,我們使用 cache.set() 來緩存結果。

打開代發服務器,訪問 http://127.0.0.1:8000 。當視圖被執行的時候,緩存鍵沒有被找到的話查詢集就會被執行。訪問 http://127.0.0.1:8000/adim/ 然后打開 memcached 統計。你可以看到類似于下面的緩存的使用數據:

django-11-7

看一眼 Curr Items 應該是 1 。這表示當前有一個內容被緩存。Get Hits 表示有多少的 get 操作成功了,Get Miss表示有多少的請求丟失了。Miss Ratio 是使用它們倆來計算的。

現在導航回 http://127.0.0.1:8000/ ,重載頁面幾次。如果你現在看緩存統計的話,你就會看到更多的(Get HitsCmd Get被執行了)

基于動態數據的緩存

有很多時候你都會想使用基于動態數據的緩存的。基于這樣的情況,你必須要創建包含所有要求信息的動態鍵來特別定以緩存數據。編輯 courses 應用的 views.py ,修改 CourseListView ,讓它看起來像這樣:

class CourseListView(TemplateResponseMixin, View):
    model = Course
    template_name = 'courses/course/list.html'
    
    def get(self, request, subject=None):
        subjects = cache.get('all_subjects')
        if not subjects:
            subjects = Subject.objects.annotate(
                            total_courses=Count('courses'))
            cache.set('all_subjects', subjects)
        all_courses = Course.objects.annotate(
            total_modules=Count('modules'))
        if subject:
            subject = get_object_or_404(Subject, slug=subject)
            key = 'subject_{}_courses'.format(subject.id)
            courses = cache.get(key)
            if not courses:
                courses = all_courses.filter(subject=subject)
                cache.set(key, courses)
        else:
            courses = cache.get('all_courses')
            if not courses:
                courses = all_courses
                cache.set('all_courses', courses)
        return self.render_to_response({'subjects': subjects,
                                        'subject': subject,
                                        'courses': courses})

在這個場景中,我們把課程和根據科目篩選的課程都緩存了。我們使用 all_courses 緩存鍵來儲存所有的課程,如果沒有給科目的話。如果給了一個科目的話我們就用 'subject_()_course'.format(subject.id)動態的創建緩存鍵。

注意到我們不能用一個緩存查詢集來創建另外的查詢集是很重要的,因為我們已經緩存了當前的查詢結果,所以我們不能這樣做:

courses = cache.get('all_courses')
courses.filter(subject=subject)

相反,我們需要創建基本的查詢集 Course.objects.annotate(total_modules=Count('modules')) ,它不會被執行除非你強制執行它,然后用它來更進一步的用 all_courses.filter(subject=subject) 限制查詢集萬一數據沒有在緩存中找到的話。

緩存模板片段

緩存模板片段是一個高級別的方法。你需要使用 {% load cache %} 在模板中載入緩存模板標簽。然后你就可以使用 {% cache %} 模板標簽來緩存特定的模板片段了。你通常可以像下面這樣使用緩存標簽:

{% cache 300 fragment_name %}
...
{% endcache %}

{% cache %} 標簽要求兩個參數:過期時間,以秒為單位,和一個片段名稱。如果你需要緩存基于動態數據的內容,你可以通過傳遞額外的參數給 {% cache %} 模板標簽來特別的指定片段。

編輯 students 應用的 /students/course/detail.html 。在頂部添加以下代碼,就在 {% extends %} 標簽的后面:

{% load cache %}

然后把下面幾行:

{% for content in module.contents.all %}
    {% with item=content.item %}
        <h2>{{ item.title }}</h2>
        {{ item.render }}
    {% endwith %}
{% endfor %}

替換為:

{% cache 600 module_contents module %}
    {% for content in module.contents.all %}
        {% with item=content.item %}
            <h2>{{ item.title }}</h2>
            {{ item.render }}
        {% endwith %}
    {% endfor %}
{% endcache %}

我們使用名字 module_contents 和傳遞當前的 Module 對象來緩存模板片段。這對于當請求不同的模型是避免緩存一個模型的內容和服務錯誤的內容來說是很重要的。

如果 USE_I18N 設置是為 True,單一站點的中間件緩存將會遵照當前激活的語言。如果你使用了 {% cache %} 模板標簽以及可用翻譯特定的變量中的一個,那么他們的效果將會是一樣的,比如:{% cache 600 name request.LANGUAGE_CODE %}

緩存視圖

你可以使用位于 django.views.decrators.cachecache_page 裝飾器來煥春輸出的單個視圖。裝飾器要求一個過期時間的參數(以秒為單位)。

讓我們在我們的視圖中使用它。編輯 students 應用的 urls.py ,添加以下 導入:

from django.views.decorators.cache import cache_page

然后按照如下在 student_course_detail_module URL 模式上應用 cache_page 裝飾器:

url(r'^course/(?P<pk>\d+)/$',
    cache_page(60 * 15)(views.StudentCourseDetailView.as_view()),
    name='student_course_detail'),

url(r'^course/(?P<pk>\d+)/(?P<module_id>\d+)/$',
    cache_page(60 * 15)(views.StudentCourseDetailView.as_view()),
    name='student_course_detail_module'),

現在 StudentCourseDetailView 的結果就會被緩存 15 分鐘了。

單一的視圖緩存使用 URL 來創建緩存鍵。多個指向同一個視圖的 URLs 將會被分開儲存

使用單一站點緩存

這是最高級的緩存。他讓你可以緩存你的整個站點。

為了使用單一站點緩存,你需要編輯項目中的 settings.py ,把 UpdateCacheMiddlewareFetchFromCacheMiddleware 添加進 MIDDLEWARE_CLASSES 設置中:

MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.cache.UpdateCacheMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.cache.FetchFromCacheMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    # ...
)

記住在請求的過程中,中間件是按照所給的順序來執行的,在相應過程中是逆序執行的。UpdateCacheMiddleware 被放在 CommonMiddleware 之前,因為它在相應時才執行,此時中間件是逆序執行的。FetchFromCacheMiddleware 被放在 CommonMiddleware 之后,是因為它需要連接后者的的請求數據集。

然后,把下列設置添加進 settings.py 文件:

CACHE_MIDDLEWARE_ALIAS = 'default'
CACHE_MIDDLEWARE_SECONDS = 60 * 15 # 15 minutes
CACHE_MIDDLEWARE_KEY_PREFIX = 'educa'

在這些設置中我們為中間件使用了默認的緩存,然后我們把全局緩存過期時間設置為 15 分鐘。我們也指定了所有的緩存鍵前綴來避免沖突萬一我們為多個項目使用了相同的 memcached 后端。我們的站點現在將會為所有的 GET 請求緩存以及返回緩存內容。

我們已經完成了這個來測試單一站點緩存功能。盡管,以站點的緩存對于我們來說是不怎么合適的,因為我們的課程管理視圖需要展示更新數據來實時的給出任何的修改。我們項目中的最好的方法是緩存用于展示給學生的課程內容的模板或者視圖數據。

我們已經大致體驗過了 Django 提供的方法來緩存數據。你應合適的定義你自己的緩存策略,優先考慮開銷最大的查詢集或者計算。

總結

在這一章中,我們創建了一個用于課程的公共視圖,創建了一個用于學生注冊和報名課程的系統。我們安裝了 memcached 以及實現了不同級別的緩存。

在下一章中,我們將會為你的項目創建 RESTful API。

(譯者 @夜夜月注:終于只剩下最后一章了!)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,835評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,676評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,730評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,118評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,873評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,266評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,330評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,482評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,036評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,846評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,025評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,575評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,279評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,684評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,953評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,751評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,016評論 2 375

推薦閱讀更多精彩內容