第十一章 渲染和缓存课程内容

在上一章中,使用了模型继承和通用关系建立弹性的课程、章节和内容的关联数据模型,并且建立了一个CMS系统,在其中使用了CBV,表单集和AJAX管理课程内容。在这一章将要做的事情是:

我们就从建立一个课程目录,供学生们浏览和选课来开始本章。

1展示课程

为了展示课程,我们需要实现如下功能:

  1. 列出所有可用的课程,可以通过课程主题来进行筛选
  2. 显示某个课程的具体内容

由于数据模型已经齐备,编辑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. 取所有的主题,使用了annotate()分组和Count()聚合方法生成一个其中包含课程的数量字段。
  2. 获得所有课程,同样进行了分组,增加了一个按照章节分组计数的字段。
  3. 如果传入了某个具体的主题slug字段,就取得该slug对应的具体主题,并且将课程设置为该主题对应的课程,而不是全部课程。
  4. 使用个TemplateResponseMixin类提供的render_to_response()方法将上边几个结果返回给模板。

再创建一个显示具体课程的视图,在views.py里增加如下内容:

from django.views.generic.detail import DetailView

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

这个视图继承了Django内置的DetailView视图,为其指定模型model和模板template_name属性,该CBV会在模板上渲染其中该数据类的详情。DetailView需要一个slug或者主键pk来从指定的Course模型中获取具体信息,然后在template_name属性指定的模板中进行渲染。

编辑educa项目的根路由urls.py文件,增加以下代码:

from courses.views import CourseListView
urlpatterns = [
    # ...
    path('', CourseListView.as_view(), name='course_list'),
]

我们想让访问该站点的人默认就来到列表页,因此将course_list设置为匹配网站的根目录,将这行放在所有URL的最下边,其他课程相关的URL都带有/course/前缀。

然后编辑courses应用的urls.py文件,增加下边两条URL:

    path('subject/<slug:subject>', views.CourseListView.as_view(), name='course_list_subject'),
    path('<slug:slug>', views.CourseDetailView.as_view(), name='course_detail'),

我们添加了如下两条路由:

来为这两个视图创建模板,在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 %}

这个模板用来列出所有的课程。模板中创建了一个列表,展示所有的Subject对象和反向解析的链接course_list_subject,使用判断来切换CSS类selected用于显示当前被选中的主题。然后迭代所有的Course对象,展示其中的总章节数目以及讲师的名字。

启动站点,打开http://127.0.0.1:8000/,可以看到如下页面:

左侧边栏包含所有的主题以及主题中的课程数量,右侧在默认情况下,显示所有的主题其中的所有的课程。如果选择任何主题,则只显示该主题对应的课程。

编辑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/,点击右侧任意一个课程,可以看到如下页面:

我们已经建立好了公共的(不需要特别权限)的展示课程的页面,下一步,需要允许用户以学生身份注册并且选课。

2增加学生注册功能

建立一个新的应用来管理学生注册功能:

python manage.py startapp students

编辑settings.py激活新应用:

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

2.1创建注册视图

编辑students目录内的views.py文件,增加如下代码:

from django.urls 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视图,该视图提供了创建模型对象的一般方法。这个视图需要如下属性:

  1. template_name:需要渲染的模板位置
  2. form_class:必须是一个ModelForm对象,这里指定为Django内置的建立新用户的UserCreationForm表单。
  3. success_url:成功后跳转的URL,通过反向解析student_course_list获取,看名字就知道是给学生列出课程列表的URL,会在稍后配置该URL。

form_valid()方法表单数据成功验证的时候执行,该方法必须返回一个HTTP响应。重写该方法以使用户在成功注册之后就登录。

students应用中创建urls.py文件,并在其中设置该视图的URL:

from django.urls import path
from . import views

urlpatterns = [
    path('register/', views.StudentRegistrationView.as_view(), name='student_registration'),
]

然后编辑educa项目的根urls.py,为students应用配置二级路由:

urlpatterns = [
    # ...
    path('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 %}

启动站点,到http://127.0.0.1:8000/students/register/,应该可以看到如下的注册表单:

注意此时StudentRegistrationView视图的success_url属性中的student_course_list URL还没有配置,所以还不能提交表单,否则会报错。会在下一节中配置该URL。

2.2选课功能

在用户注册成功之后,应该能够让其选课以便加入到某门课的学习中去。很显然,一个学生可以选择多门课程,一个课程也有多个学生,需要在Course模型和User模型之间添加一个多对多关系。

编辑courses应用的models.py文件,为Course模型添加一个字段:

class Course(models.Model):
    # ......
    students = models.ManyToManyField(User, related_name='courses_joined', blank=True)

之后立刻执行数据迁移过程,不再赘述

现在我们可以通过多对多关系来设置学生与课程之间的关系了。之后需要编写一个视图用于实现选课功能。

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 django.contrib.auth.mixins 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视图。该视图继承了内置LoginRequiredMixin类,一定要用户登录才能使用该功能。还继承了内置的FormView,因为我们要处理表单提交。设置form_class属性为CourseEnrollForm类,设置了一个course属性用于保存学生选的课程对象。当表单验证通过的时候,取得当前的用户,设置多对多关系,然后调用父类的方法保存数据。

get_success_url()方法返回了成功之后跳转的URL,这个方法和success_url属性的功能一样。该URL会在下一节中设置。

编辑students应用中的urls.py文件,为该视图配置URL:

    path('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(CourseDetailView, self).get_context_data(**kwargs)
        context['enroll_form'] = CourseEnrollForm(initial={'course':self.object})
        return context

这里重写了get_context_data()方法,把这个表单添加到模板变量中,并且将表单中隐藏的字段内容初始化成了当前的课程对象,所以可以直接通过按钮提交表单,无需填写隐藏字段。

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的隐藏表单,如果未登录,展示一个登录链接供用户登录。

启动站点,在浏览器中打开http://127.0.0.1:8000/,然后点击一个具体的课程,如果已经登录,可以看到该按钮,如下所示:

如果未登录,则看到的是一个REGISTER TO ENROLL的按钮。

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(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)
        course = self.get_object()

        if 'module_id' in self.kwargs:
            context['module'] = course.modules.get(id=self.kwargs['module_id'])
        else:
            context['module'] = course.modules.all()[0]
        return context

这个视图用于向学生展示他们选的课程和章节。依然重写了get_queryset()方法用于返回与当前学生已选课程。重写了get_context_data()方法,如果给了一个module_id,就将模板变量module设置为这个module_id对应的课程,如果没给出,默认为该课程的第一个章节。这样学生就能浏览整个课程的章节。

然后在students应用中的urls.py中为该视图配置URL:

    path('course/',views.StudentCourseListView.as_view(),name='student_course_list'),
    path('course/<pk>/',views.StudentCourseDetailView.as_view(),name='student_course_detail'),
    path('course/<pk>/<module_id>/',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 %}

这个模板用于展示用户所有选的课程。注意在上一小节里,学生注册成功之后,会被重定向至student_course_list URL,但是如果在其他页面登录,会被导向内置的验证模块的相关URL,所以修改settings.py

from django.urls import reverse_lazy
LOGIN_REDIRECT_URL = reverse_lazy('student_course_list')

设置成这样之后,所有内置auth模块完成登录操作之后都跳转到该指定地址。现在所有的学生在注册成功之后都会跳转到student_course_list URL,即显示该学生已选课程的页面。

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

这是用于让学生具体学习已选课程内容的页面。首先我们创建了一个列表包含所有章节,并且高亮当前章节,然后迭代所有当前章节中的内容,使用了一个{{ item.render }}来展示具体的内容。

此时render()方法还没有编写,在下一节中就来为每个内容对象编写这个方法来展示不同种类的内容。

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()方法,传入一个模板名称和上下文,然后模板渲染成为一个HTML字符串。每种类型的内容采用不同名称的模板。使用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><img src="{{ item.file.url }}"></p>

由于ImageFieldFileField都是文件字段,为了管理这两个字段,必须在settings.py中配置媒体文件的路径:

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

回忆一下,这里MEDIA_URL是指上传媒体文件的路径,MEDIA_ROOT是指的查找媒体文件的路径。

编辑项目的根urls.py文件,在开头添加下列导入代码:

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

然后在末尾追加:

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

现在我们的站点就能够接受文件上传和提供文件存储了。在开发的过程中,Django会管理媒体文件。但在正式生产环境中,就不能如此配置了。我们将在第十三章学习生产环境的配置。

关于video.html,有点额外的事情要做。将使用django-embed-video模块来集成视频内容。django-embed-video是一个第三方模块,可以将来自YouTube或者Vimeo等来源的视频内容集成到模板中,只需为其提供视频的URL即可。

安装该模块:

pip install django-embed-video==1.1.2

然后在settings.py里激活该应用:

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

可以在https://django-embed-video.readthedocs.io/en/latest/找到这个模块的文档。

现在来编辑video.html

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

现在启动站点,到http://127.0.0.1:8000/course/mine/,以Instructors组内用户的身份登录,为一个课程添加各种课程内容,视频地址可以拷贝任意的YouTube链接比如 https://www.youtube.com/watch?v=bgV39DlmZ2U

在添加完内容之后,到http://127.0.0.1:8000/点击刚创建的课程,再点击Enroll Now,之后被重定向到课程列表,应该可以看到类似下面的页面:

这样就完成了展示内容的通用方法。

译者注:这里还有一些不完善的地方,比如选了某个课之后回到列表页面再次进入已经选过的课程,会看到Enroll Now还在,其实应该显示Start to Learn之类的词语。这只要在模板中检测一下当前的课程是否包含在用户已经选择的课程中就可以了。

此外在测试代码的时候还发现,如果一个课程内没有章节,则作者在StudentCourseDetailView中的最后一句:

    context['module'] = course.modules.all()[0]

此处硬编码了返回第一个查询结果,就会报错,修改方法是做个判断,如果长度=0,就返回空就可以了,这样页面不会渲染出内容。

4使用缓存框架

HTTP请求对我们的Web应用来说,意味着查询数据和处理业务逻辑和渲染模板等工作,这比返回一个静态的页面开销要大很多。

当站点的流量越来越大的时候,大量访问给后端带领的压力是巨大的。这个时候就是缓存系统大派用场的时刻。把一个HTTP请求导致的数据查询,业务逻辑处理结果,甚至渲染后的内容缓存起来,就可以避免在后续类似的请求中反复执行开销大的操作,会有效地提高网站的响应时间。

Django包含一个健壮的缓存系统,可以缓存不同粒度的数据。可以缓存一个查询,一个视图的返回结果,部分模板的渲染内容,甚至整个站点。在缓存系统中存储的数据有时效性,可以设置其过期的时间。

当应用接到一个HTTP请求的时候,通常按照如下的顺序使用缓存系统:

  1. 在缓存系统中寻找HTTP请求需要的数据
  2. 如果找到了,返回缓存的数据
  3. 如果没有找到,按照如下顺序执行:
    1. 进行数据查询或者处理,得到数据
    2. 将数据保存在缓存内
    3. 返回数据

关于详细的缓存机制,可以官方文档:https://docs.djangoproject.com/en/2.0/topics/cache/

4.1可用的缓存后端

就像数据库一样,Django的缓存机制可以使用多种缓存服务后端来完成,主要有这些:

  1. backends.memcached.MemcachedCachebackends.memcached.PyLibMCCache:是基于Memcached服务的后端。具体使用哪种后端取决于采用哪种Python支持的Memcached模块。
  2. backends.db.DatabaseCache:使用数据库作为缓存(还记得Redis吗)
  3. backends.filebased.FileBasedCache:使用文件作为缓存,序列化每个缓存数据为一个单独的文件
  4. backends.locmem.LocMemCache:本地内存缓存,这是默认值。
  5. backends.dummy.DummyCache:伪缓存机制,仅用于开发。提供了缓存界面但实际上不缓存任何内容。每个进程的缓存互相独立而且线程安全。

对于优化性能而言,最好选取基于内存缓存的缓存机制比如Memcached后端。

4.2安装Memcached服务

我们将使用Memcached缓存。Memcached在内存中运行,占用一定大小的内存作为缓冲区。当被分配的内存用光的时候,Memcached就会以新数据替代较老的数据。

https://memcached.org/downloads下载Memcached,如果是Linux系统,可以使用下列命令编译安装:

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

如果使用MacOS X而且安装了Homebrew,可以直接通过brew install memcached安装,也可以在https://brew.sh/下载。安装了Memcached之后,可以通过一个命令启动服务:

memcached -l 127.0.0.1:11211

此时Memcached就会在默认的11211端口运行。还可以通过-l参数指定其他的主机和端口号。可以在https://memcached.org查看文档。

还需要安装Python的Memcached模块:

pip install python-memcached==1.59

4.3缓存设置

Django提供了下列缓存设置:

通过CACHES可以设置项目的缓存系统。这个设置以字典的形式,可以配置多个缓存后端的设置。每个CACHES中的缓存后端有如下设置:

4.4为项目配置Memcached缓存

编辑settings.py,将上述的缓存设置加入到文件中:

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

这里指定了后端为Memcached,然后指定了主机IP和端口号。如果有很多Memcached配置在不同主机上,可以给LOCATION传一个列表。

4.4.1监控Memcached服务

为了监控Memcached的服务,可以使用第三方模块django-memcache-status,该模块可以在管理后台中显示Memcached的统计情况。安装该模块:

pip install django-memcache-status==1.3

编辑setting.py,激活该应用:

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

确保Memcached服务在运行,然后进入站点的管理后台,可以看到如下的内容:

绿色的条代表空闲的缓存容量,红色代表已经使用的内容。如果点击标题,可以展开一个详情页看具体内容。已经为项目配置好了Memcached服务,现在来缓存具体内容。

4.5缓存级别

Django为不同粒度的数据提供了如下的缓存级别:

  1. Low-level cache API:粒度最细,缓存精确的查询或者计算结果
  2. Per-view cache:对单独的视图进行缓存
  3. Template cache:缓存模板片段
  4. Per-site cache:站点缓存,最高级别缓存。

在建立缓存系统之前,必须仔细考虑缓存策略。建议对不基于具体用户身份的,系统开销比较大的数据库查询和密集计算进行缓存

4.6使用Low-level cache API

Low-level缓存API可以存储任意粒度的对象,该功能位于django.core.cache,可以导入进来,:

from django.core.cache import cache

这个缓存默认使用会使用配置中的default名称对应的缓存后端,类似于数据库,等于通过caches['default]获取缓存后端。

可以通过如下命令得到一个使用某个具体配置名称的缓存配置:

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

在导入当前Django环境的Python命令行中里进行一些实验:

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

通过使用了set(key,value,timeout)方法,向默认的缓存后端存入了一个键名叫'musician',值是'Django Reinhardt',20秒过期。如果不给出具体时间,则Django使用settings.py中的默认设置。然后再输入:

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

可以获取对应的值。等待20秒再执行上述命令:

>>> cache.get('musician')

说明该键已经过期,get()方法返回None

不要在缓存中存储值为None的键,否则无法区分缓存命中与否。

再实验如下代码:

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

这里先执行了一个QuerySet查询,然后将查询的结果缓存到all_subjects键中。来试试从缓存中取数:

>>> cache.get('all_subjects')
<QuerySet [<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)

在这段代码里,我们先尝试去缓存中获取all_subjects这个键,如果结果为None,说明缓存中没有,执行正常数据查询,然后将结果存入到缓存中。

启动站点,访问http://127.0.0.1:8000/,只要访问这个路径,刚才配置的缓存就会启动,由于第一次执行,之前没有缓存内容,所以视图就会将查询结果放入缓存。此时进入管理后台查看Memcached的统计,可以看到如下内容:

在Memcache的统计数据里找到Curr Item这一项,如果严格按照本文来进行,应该为1,除非之前存储了其他内容。这表示当前缓存中有一个键值对。Get Hits表示有多少次Get操作成功命中缓存数据,Get Miss则表示未命中的次数。最上边的Miss Ration使用这两个值计算得到。

现在打开浏览器,反复刷新http://127.0.0.1:8000/,然后再去看Memcahed的统计页面,看看统计数据的变化。

4.6.1缓存动态数据

但有的时候,想缓存动态生成的数据。这就必须建立动态的键,用于唯一确定对具体的缓存数据。看以下例子:

编辑courses应用的views.py文件,修改CourseListView视图如下所示:

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)'对应着具体类别的查询结果集。

要注意的是,不能用从缓存中取出来的查询结果再去建立其他查询结果,也就是说下边的代码是不行的:

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

缓存只能用于存储最终可供页面直接使用的查询结果,不能在中间步骤缓存。所以这就是为什么要在视图开始的地方建立基础查询all_courses = Course.objects.annotate(total_modules=Count('modules')),然后再用courses = all_courses.filter(subject=subject)生成查询结果的原因。

4.7缓存模板片段

缓存模板片段是比较高级别的缓存,需要在模板中加载缓存标签:{% load cache %},然后使用{% cache %}来标记需要缓存的片段。实际使用像这样:

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

如上边例子所示,{% cache %}标签有两个可选的参数,第一个是过期秒数,第二个是为该片段起的名称。如果需要缓存动态生成的模板片段,可以再增加额外的参数用于生成唯一KEY。

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

这里使用了600秒的过期时间,指定了该片段的别名为module_contents,然后用module变量动态创建键,这样就建立了独特的键以避免重复。

如果启用了国际化设置USE_I18N=True,缓存中间件会考虑语言的影响。如果你在一个页面中使用了{% cache %}标签,下次想从缓存中拿到正确的数据,必须将特定语言的代码和缓存标签一起使用,才能得到正确的结果:例如{% cache 600 name request.LANGUAGE_CODE %}.

4.8缓存视图

可以通过使用django.views.decorators.cache中的cache_page装饰器来缓存视图的输出结果,需要一个参数timeout,是过期秒数。

在视图中使用该装饰器,编辑students应用的urls.py文件,先导入该装饰器:

from django.views.decorators.cache import cache_page

然后把cache_page用于student_course_detailstudent_course_detail_module两个URL上,如下:

path('course/<pk>/', cache_page(60 * 15)(views.StudentCourseDetailView.as_view()), name='student_course_detail'),
path('course/<pk>/<module_id>/', cache_page(60 * 15)(views.StudentCourseDetailView.as_view()),
     name='student_course_detail_module'),

这样配置之后,StudentCourseDetailView的结果就会被缓存15分钟。

注意,缓存使用URL来构建缓存键,对同一个视图函数,来自不同URL路由的结果,会被分别缓存。

4.8.1缓存动态数据

缓存站点是级别最高的缓存,允许缓存整个站点。

要启用站点缓存,需要编辑settings.py,把UpdateCacheMiddlewareFetchFromCacheMiddleware中间件加入MIDDLEWARE设置中:

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

中间件的顺序至关重要,中间件在HTTP请求进来的时候是按照从上到下的顺序执行,返回响应的时候按照从下到上的顺序执行。UpdateCacheMiddleware必须放在CommonMiddleware的上边,因为UpdateCacheMiddleware只在响应的时候才执行。FetchFromCacheMiddleware被放在CommonMiddleware之后,因为FetchFromCacheMiddleware需要CommonMiddleware处理过的请求数据。

然后还需要把下列设置加入到settings.py中:

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

在这些设置里,设置了使用default名称的缓存后端,15分钟过期时间,以及设置前缀避免重复。现在站点对所有的GET请求,都缓存和优先返回缓存的结果。

这样我们就设置好了整个站点的缓存,然而站点缓存对于我们这个站来说是不适合的,因为CMS系统里更改了数据之后,必须立刻返回更新后的数据。所以最佳的方法是缓存向学生返回课程内容的视图或者模板。

我们已经学习过了Django内的各种方法用于缓存数据。应该明智的设置缓存策略,优先缓存开销高的查询和计算。

总结

在这一章,我们创建了公开展示所有课程的页面,通过多对多关系建立了学生注册和选课系统,并且为站点安装了Memcached服务并缓存了各个级别的内容。

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