第十一章 缓存内容

11 缓存内容

在上一章中,你使用模型继承和通用关系来创建灵活的课程内容模型。你还使用基于类的视图,表单集和AJAX排序内容创建了一个课程管理系统。在本章中,你会学习学习以下内容:

  • 创建显示课程信息的公开视图
  • 构建一个学生注册系统
  • 在课程中管理学生报名
  • 渲染不同的课程内容
  • 使用缓存框架缓存内容

我们从创建课程目录开始,让学生可以浏览已存在的课程,并且可以报名参加。

11.1 显示课程

对于我们的课程目录,我们需要构建以下功能:

  • 列出所有可用课程,可用通过主题过滤
  • 显示单个课程的概述

编辑courses应用的views.py文件,添加以下代码:

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

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中指定的模板,并在上下文中包括object对象。

编辑educa项目的主urls.py文件,并添加以下代码:

from courses.views import CourseListView

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

因为我们想在http://127.0.0.1:8000/显示课程列表,而courses应用的其它所有URL带/course/前缀,所以我们在项目的主urls.py文件中添加course_list URL模式。

编辑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_subject:显示单个课程的概述

让我们为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 title %}

{% 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>
                    {{ coursse.total_modules }} modules.
                    Instructor: {{ course.owner.get_full_name }}
                </p>
            {% endwith %}
        {% endfor %}
    </div>
{% endblock content %}

这是显示可用课程列表的模板。我们创建了一个HTML列表显示所有Subject对象,并为每个Subject对象构建一个到course_list_subject URL的链接。我们点击了selected类高亮显示当前主题(如果存在的话)。我们迭代每个Course对象,显示单元总数和教师姓名。

使用python manage.py runserver命令启动开发服务器,并在浏览器中打开http://127.0.0.1:8000/。你会看到类似以下的页面:

左侧边栏包括所有主题,已经每个主题的课程总数。你可以点击任何一个主题来过滤显示的课程。

编辑courses/course/detail.html模板,并添加以下代码:

{% extends "base.html" %}

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

{% 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.
                Instructors: {{ course.owner.get_full_name }}
            </p>
            {{ object.overview|linebreaks }}
        </div>
    {% endwith %}
{% endblock content %}

在这个模板中,我们显示单个课程的概述和详情。在浏览器中打开http://127.0.0.1:8000/,然后点击其中一个课程。你会看到以下结构的页面:

我们已经创建了展示课程的公开区域。接下来,让我们允许用户注册为学生,并报名参加课程。

11.2 添加学生注册

使用以下命令创建一个新应用:

python manage.py startapp students

编辑educa项目的settings.py文件,把students添加到INSTALLED_APPS设置中:

INSTALLED_APPS = [
    # ...
    'students',
]

11.2.1 创建学生注册视图

编辑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().form_valid(form)
        cd = form.cleaned_data
        user = authenticate(
            username=cd['username'],
            password=cd['password']
        )
        login(self.request, user)
        return result

这个视图允许学生在我们网站上注册。我们使用通用的CreateView,它提供了创建模型对象的功能。这个视图需要以下属性:

  • template_name:用于这个视图的模板的路径。
  • form_class:创建对象的表单,必须是一个ModelForm。我们使用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应用的URL添加到URL配置中:

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

students应用中创建以下文件结构:

templates/
    students/
        student/
            registration.html

编辑students/student/registration.html模板,并添加以下代码:

{% extends "base.html" %}

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

{% 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 content %}

最后,编辑educa项目的settings.py文件,并添加以下代码:

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

一次成功登录后,如果请求中没有next参数,则auth模块用这个设置重定义用户。

启动开发服务器,并在浏览器中打开http://127.0.0.1:8000/students/register/。你会看到以下注册表单:

11.2.2 报名参加课程

用户创建账户之后,他们应该可以报名参加课程。为了存储花名册,我们需要在CourseUser模型之间创建多对多关系。编辑courses应用的models.py文件,并在Course模型中添加以下字段:

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

在终端中执行以下命令,为这个修改创建一个数据库迁移:

python manage.py makemigrations

你会看到类似这样的输出:

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

然后执行以下命令,应用数据库迁移:

python manage.py migrate

你会看到以下输出:

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, courses, sessions
Running migrations:
  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().form_valid(form)

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

这是StudentEnrollCourseView视图。它处理学生报名课程。这个视图从LoginRequiredMixin继承,所以只有登录的用户才可以访问这个视图。因为我们需要处理表单的提交,所以它还从Django的FormView继承。我们为form_class属性使用CourseEnrollForm表单,还定义了存储给定Course对象的course属性。当表单有效时,我们添加当前用户到课程的注册学生中。

get_success_url()方法返回一个URL,如果表单提交成功,则重定向用户到这个URL。这个方法等同于success_url属性。我们逆向之后会创建的student_course_detail URL,用来显示课程内容。

编辑students应用的urls.py文件,添加以下URL模型:

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

让我们在课程概述页面添加报名按钮表单。编辑courses应用的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().get_context_data(**kwargs)
        context['enroll_form'] = CourseEnrollForm(
            initial={'course': self.object}
        )
        return context

我们使用get_context_data()方法在上下文中包括报名表单,用于渲染模板。我们使用当前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按钮,如下图所示:

如果你没有登录,则会看到Register to enroll按钮。

11.3 访问课程内容

我们需要一个视图显示学生参加的课程,以及一个访问实际课程内容的视图。编辑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().get_queryset()
        return qs.filter(students__in=[self.request.user])

这是视图列出学生报名参加的课程。它从LoginRequiredMixin继承,确保只有登录的用户才能访问这个视图。它还从通用的ListView继承,显示Course对象列表。我们覆写get_queryset()方法,只检索用户报名的课程:我们用students字段过滤QuerySet。

然后在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().get_queryset()
        return qs.filter(students__in=[self.request.user])

    def get_context_data(self, **kwargs):
        context = super().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()方法来限制QuerySet为用户报名的课程。我们还覆写了get_context_data()方法,如果给定了URL参数module_id,则在上下文中设置一个课程单元。否则我们设置课程的第一个单元。这样,学生可以在课程中浏览各个单元。

编辑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 title %}

{% 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 content %}

这个模板显示用户报名的课程。编辑students/course/detail.html模板,并添加以下代码:

{% extends "base.html" %}

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

{% 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 content %}

报名的学生通过这个模板访问一个课程的内容。首先,我们构建了一个包括所有课程单元的HTML列表,并高亮显示当前单元。然后我们迭代当前单元内容,并用{{ item.render }}访问每个内容项来显示它。接下来我们会添加render()方法到内容模型中。该方法负责适当的渲染内容。

11.3.1 渲染不同类型的内容

我们需要提供一种方式来渲染每种类型的内容。编辑courses应用的models.py文件,并在ItemBase模型中添加render()方法:

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构建适当的模板名称。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>

要使用ImageFieldFileField上传文件,我们需要设置项目用开发服务器提供多媒体文件服务。编辑项目的settings.py文件,并添加以下代码:

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

记住,MEDIA_URL是提供多媒体文件上传服务的基础URL,而MEDIA_ROOT是存储文件的本地路径。

编辑项目的主urls.py文件,添加以下导入:

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 install django-embed-video

然后编辑项目的settings.py文件,把embed_video添加到INSTALLED_APPS设置中。你可以在这里查看django-embed-video文档。

编辑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?v=bgV39DlmZ2U)到表单的url字段。添加内容到课程后,打开http://127.0.0.1:8000/,点击课程,然后点击ENROLL NOW按钮。你会报名参加课程,并重定向到student_course_detail URL。如下图所示:

非常棒!你已经为渲染课程内容创建了一个通用接口,每种课程内容都已特定方式渲染。

11.4 使用缓存框架

对Web应用的HTTP请求通常需要数据库访问,数据处理和模板渲染。在处理方法,它们的开销比静态网站大多了。

当网站的流量变得越来越大,有些请求的开销可能会很大。此时缓存变得很有意义。通过在HTTP请求中缓存查询,计算结果或者渲染的内容,你会之后的请求中避免昂贵的操作。这意味着服务端更短的响应时间和更少的处理。

Django包括一个健壮的缓存系统,允许你用不同级别的粒度缓存数据。你可以缓存单个查询,一个特定视图的输出,部分被渲染模板的内容,或者整个网站。内容会在默认时间内存储在缓存系统中。你可以为缓存的数据指定默认的超时时间。

当你的应用收到一个HTTP请求时,通常会这样使用缓存框架:

  1. 尝试在缓存中查找请求的数据。
  2. 如果找到,则返回缓存数据。
  3. 如果没有找到,则执行以下步骤:
    • 执行获取数据所需的查询或处理。
    • 在缓存中保存生成的数据。
    • 返回数据

你可以在这里阅读Django缓存系统的详细信息。

11.4.1 可用的缓存后台

Django自带数个缓存后台,分别是:

  • backends.memcached.MemcachedCachebackends.memcached.PyLibMCCache:一个Memcached后台。Memcached是一个快速和高效的基于内存的缓存服务。使用的后台取决于你选择的Python绑定的Memcached。
  • backends.db.DatabaseCache:使用数据库作为缓存系统。
  • backends.filebased.FileBasedCache:使用文件存储系统。在单个文件中序列号和存储每个缓存值。
  • backends.locmem.LocMemCache:本地内存缓存后台。这是默认的缓存后台。
  • backends.dummy.DummyCache:一个只适用于开发的缓存后台。它实现了缓存接口,但不会真正缓存任何数据。这个缓存是预处理和线程安全的。

使用基于内存的缓存后台,比如Memcached,会有最优的性能。

11.4.2 安装Memcached

我们将使用Memcached后台。Memcached在内存中运行,并分配了一定数量的RAM。当分配的RAM满了之后,Memcached会移除最旧的数据。

这里下载Memcached。如果你使用的是Linux,你可以使用以下命令安装Memcached:

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

如果你使用的是Mac OS X,你可以使用brew install Memcached命令安装。你可以在这里下载Homebrew。

如果你使用的是Windows,你可以在这里找到Windows二进制版本。

**译者注:**Windows版本的链接已经失效。

安装Memcached后,打开终端,并使用以下命令启动:

memcached -l 127.0.0.1:11211

Memcached默认在11211端口运行。但你可以使用-l选项指定主机和端口。你可以在这里查看更多关于Memcached的信息。

安装Memcached后,你需要安装它的Python绑定。使用以下命令完成安装:

pip install python3-memcached

11.4.3 缓存设置

Django提供了以下缓存设置:

  • CACHES:包括项目中所有可用缓存的字典。
  • CACHE_MIDDLEWARE_ALIAS:用于存储的缓存别名。
  • CACHE_MIDDLEWARE_KEY_PREFIX:用于缓存键的前缀。如果你在数个网站中共享同一个缓存,可用设置前缀来避免键冲突。
  • CACHE_MIDDLEWARE_SECONDS:缓存页面的默认秒数。

可用使用CACHES设置来配置项目的缓存系统。这个设置是一个字典,允许你配置多个缓存。CACHES字典中每个缓存可用指定以下数据:

  • BACKEND:使用的缓存后台。
  • KEY_FUNCTION:一个包括点号路径的可调用对象的字符串,可调用对象接收prefixversionkey作为参数,返回最终的缓存键。
  • KEY_PREFIX:所有缓存键的字符串前缀,避免冲突。
  • LOCATION:缓存的位置。根据缓存后台,它可能是一个目录,一个主机和端口,或者内存后台的命名。
  • OPTIONS:传递给缓存后台的任何额外参数。
  • TIMEOUT:存储缓存键的默认超时时间(单位秒)。默认是300秒,也就是5分钟。如果设置为None,缓存键永远不会过期。
  • VERSION:缓存键的默认版本号。用于缓存的版本控制。

11.4.4 添加Memcached到项目中

让我们为项目配置缓存。编辑educa项目的settings.py文件,并添加以下代码:

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

我们使用MemcachedCache后台。我们使用address:port语法之定义它的位置。如果你有多个Memcached实例,你可以为LOCATION使用列表。

11.4.4.1 监控Memcached

有一个django-memcache-status第三方包,可以在管理站点显示Memcached实例的统计信息。为了兼容Python3,使用以下命令从分支中安装:

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

译者注:最新版已经兼容Python3,可以直接用pip命令安装。

编辑项目的settings.py文件,把memcache_status添加到INSTALLED_APPS设置中。确保Memcached正在运行中,并在另一个终端打开开发服务器,然后在浏览器中打开http://127.0.0.1/8000/admin/。使用超级用户登录管理站点,你会看到以下区域块:

这张图片显示了缓存的使用情况。绿色代表空闲缓存,红色代表已使用的空间。如果你点击标题,则会显示Memcached实例的详细统计。

我们已经为项目设置了Memcached,并且可以监控它。让我们开始缓存数据!

11.4.5 缓存级别

Django提供了以下缓存级别,按粒度的升序排列:

  • Low-level cache API:提供了最细的粒度。允许你缓存具体的查询或计算。
  • Pre-view cache:为单个视图提供缓存。
  • Template cache:允许你缓存模板片段。
  • Pre-site cache:最高级别缓存。它缓存整个网站。

实现缓存之前,请仔细考虑你的缓存策略。首先考虑费时的查询,或者不是基于单个用户的计算。

11.4.6 使用低级别的缓存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打开终端,然后执行以下代码:

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

我们访问默认缓存后台,并使用set(key, value, timeout)存储misician键20秒,它的值是字符串Django Reinhardt。如果我们每页指定超时,则Django会使用CACHE设置中为缓存后台指定的默认超时。现在执行以下代码:

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

我们从缓存中检索键。等待20秒,然后执行同样的代码:

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

musician缓存键已经过期,get()函数返回None,因为键已经不再缓存中。

避免在缓存键中存储None,因为你不能区分实际值和缓存丢失。

让我们缓存一个QuerySet:

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

我们在Subject模型上执行了一个QuerySet,并在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_subjects键。如果没有找到给定的键则返回None。如果没有找到键(还没有缓存,或者缓存了,但是超时了),我们执行查询检索所有Subject对象和它们的课程数量,然后使用cache.set()缓存结果。

启动开发服务器,并在浏览器中打开http://127.0.0.1:8000/。当执行视图时,没有找到缓存键,并且会执行QuerySet。在浏览器中打开http://127.0.0.1:8000/admin/,并展开Memcached统计。你会看到缓存使用数据,如下图所示:

看一眼Curr Items,现在是1。它表示当前缓存中存储了一条记录。Get Hits表示成功执行了多少次get命令,Get Misses表示键的多少次get请求丢失了。Miss Ratio是这两项值计算出来的结果。

现在回到http://127.0.0.1:8000/,然后重新加载几次页面。如果你现在看一眼缓存统计,会看到更多的读取(Get HitsCmd Get增加了)。

11.4.6.1 基于动态数据缓存

很多时候,你会想要基于动态数据缓存一些东西。这种情况下,你需要动态构建包含所有信息的键,来唯一识别缓存的数据。编辑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_{}_courses'.format(subject.id)动态构建键。

请注意,我们不能使用缓存的QuerySet来构建另一个QuerySet,因为我们缓存的是QuerySet的实际结果。所以我们不能这么做:

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

相反,我们必须创建一个基本的QuerySet:Course.objects.annotate(total_modules=Count('modules')),它在你强制执行之前不会执行。当在缓存中没有找到数据时,使用all_courses.filter(subject=subject)进一步限制QuerySet。

11.4.7 缓存模板片段

缓存模板片段是一个更高级别的方法。你需要在模板中使用{% load cache %}加载缓存模板标签。然后你才可以使用{% cache %}模板标签缓存指定的模板片段。通常你会这样使用模板标签:

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

{% cache %}有两个必需的参数:超时时间(单位秒)和片段的名称。如果你需要缓存基于动态数据的内容,你可以传递额外的参数给{% cache %}模板标签,来唯一识别片段。

编辑students应用的/students/course/detail.html模板。在{% extend %}标签之后添加以下代码:

{% 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 %}

11.4.8 缓存视图

你可以使用cache_page装饰器缓存单个视图的输出,它位于django.views.decorators.cache中。该装饰器需要一个超时参数(单位秒)。

让我们在视图中使用它。编辑students应用的urls.py文件,添加以下导入:

from django.views.decorators.cache import cache_page

然后在student_course_detailstudent_course_detail_module模式上应用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构建缓存键。多个指向同一个视图的URL会分别缓存。

11.4.81 使用整个站点缓存

这是最高级别的缓存。它允许你缓存整个站点。

编辑项目的settings.py文件,在MIDDLEWARE设置中添加UpdateCacheMiddlewareFetchFromCacheMiddleware类,来启用整个站点缓存:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.cache.UpdateCacheMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.cache.FetchFromCacheMiddleware',
    # ...
]

记住,在解析请求的过程中,中间件按给定的顺序执行;而在解析响应的过程中,则是逆序执行。UpdateCacheMiddleware放在CommonMiddleware之前是因为它在响应的时候执行,此时中间件是逆序执行的。FetchFromCacheMiddleware特意放在CommonMiddleware之后,是因为它需要访问后者设置的请求数据。

然后,在settings.py文件中添加以下设置:

CACHE_MIDDLEWARE_ALIAS = 'default'
CACEH_MIDDLEWARE_SECONDS = 60 * 15 # 15 minutes
CACHE_MIDDLEWARE_KEY_PRIFIX = 'educa'

在这些设置中,我们为缓存中间件使用默认缓存,并设置全局缓存超时为15分钟。我们还为所有缓存键指定一个前缀,来避免在多个项目中使用同一个Memcached后台时的冲突。现在我们的网站会缓存数据,并为所有GET请求返回缓存的数据。

我们已经测试了整个站点缓存功能。但是,整个站点缓存不适合我们,因为课程管理视图需要立即显示更新的数据。我们项目中最好的方式是缓存模板,或者用于显示课程内容的视图。

我们已经学习了Django提供的缓存数据的方法。你应该明智的定义缓存策略,优先考虑开销最大的QuerySet或者计算。

11.5 总结

在这章中,我们为课程创建了公开的视图,并构建了一个学生注册和报名课程的系统。我们安装了Memcached,并实现了不同的缓存级别。

下一章我们会为项目构建RESTful API。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值