Django 2 By Example 在线教育平台–-学生注册和选课、运用缓存框架

Django 2 By Example 在线教育平台–-学生注册和选课、运用缓存框架

渲染和缓存课程内容 在第十章中,使用了模型继承和通用关系建立弹性的课程,章节到内容的数据模型,并且建立了一个CMS系统,在其中使用了CBV,表单集和AJAX管理所有的数据模型。在这一章将要做的事情是: 建立公开对外展示课程的视图 建立学生注册系统 学生选课系统 渲染不同的课程内容 采用缓存框架进行缓

渲染和缓存课程内容

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

  • 建立公开对外展示课程的视图
  • 建立学生注册系统
  • 学生选课系统
  • 渲染不同的课程内容
  • 采用缓存框架进行缓存内容

目前,有权限操作CMS系统的是系统管理员和讲师,如果尝试以其他用户身份操作CMS,因为没有权限会被提示403 Forbidden。我们就从建立一个课程目录供学生们浏览和选课的系统来开始本章。

展示课程

展示课程的主要功能有:

  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继承了TemplateResponseMixin和View,执行了如下任务:

  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,为其指定数据类和模板,该CBV会在模板上渲染其中该数据类的详情。DetailView需要一个slug或者主键id来从指定的Course类中获取具体信息,然后在template_name属性指定的模板中进行渲染。

编辑项目的根路由,增加以下代码:

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

我们想让访问该站点的人默认就来到列表页,将这行放在所有URL的最下边,因为其匹配的内容最宽泛。

然后编辑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'),

通过上述URL设置,站点根路径对应着展示所有课程和内容的页面,subject/slug对应某个具体的主题及课程,单独的slug则对应具体的课程。下一步是建立刚才两个视图使用的模板。

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

这个模板用来列出所有的课程。建立了一个UL列表,存放所有的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 %}

这个模板中显示了一个课程的整体情况和其中的具体内容。由于URL中传入了slug,所以就展示这个课程及内容的详情。回到 http://127.0.0.1:8000/ 点击右侧任意一个课程,可以看到如下结构的页面:

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

增加学生注册功能

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

python manage.py startapp students

编辑settings.py激活新应用:

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

建立学生注册用视图

编辑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

这是用于学生注册view,继承了内置的CreateView,提供了建立模型对象的一般方法。这个视图需要如下属性:

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

重写的form_valid()首先调用了父类的.form_valid()方法用于验证,然后直接用当前的用户名和表单中的密码1进行验证,并且让用户登录。重写后的该方法提供的功能就是用户在成功注册之后就默认是登录状态。

在students应用中建立urls.py,并在其中设置:

from django.urls import path
from . import views

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

然后编辑项目的根urls.py,为student相关功能增加URL:

    path('students/', include('students.urls')),

注意这一行要加在CourseListView的上边。

之后在students应用中建立如下目录和模板文件:

templates/
    students/
        student/
            registration.html

然后编辑 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/ ,应该可以看到如下的注册表单:

注意此时student_course_list URL还没有得到配置,所以还不能提交表单,否则会报错。会在让学生用户访问课程内容的时候配置。

选课

在学生注册成功之后,应该能够让其选课以便加入到某门课的学习中去。很显然,一个学生可以选择多门课程,一个课程也有多个学生,所以需要在Course类和User类之间添加一个多对多关系。由于User类是内置的,Course类由我们自行编写,所以将这个多对多外键设在Course类中。

编辑courses应用的models.py,找到Course类,为其添加一个字段:

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

修改model后立刻执行:

python manage.py makemigrations

看到如下字样:

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

然后执行:

python manage.py migrate

看到如下输出:

Applying courses.0004_course_students... OK

执行migration系列操作真是占了不少的篇幅。。。现在我们可以通过多对多关系来设置学生与课程之间的关系了。之后的操作,就是实现学生注册的功能。

学生注册某门课程,一定是需要在课程页面上点击一个选课的按钮,需要提交一些数据到后端,所以先来建立一个简单的表单用于学生点击选该门课程,在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])

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

get_success_url()方法返回了成功之后跳转的URL,会在下一节中设置。

编辑students应用中的urls.py,为该视图增加一行配置:

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

这样就给页面添加上了按钮,实际的被初始化为当前页面的Course对象的表单选项是隐藏的。如果用户登录,就展示该按钮,如果未登录,就让用户去登录。

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

如果未登录,则看到的是一个Register to Enroll的按钮。

访问课程内容

在学生选好课之后,还必须创建一个视图给学生展示课程中的章节和内容,以及让他们访问内容来进行具体学习,这些页面与CMS的页面很相似,只是不带有任何修改功能。

编辑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()方法,让其返回的是当前学生选择的课程而不是全部的课程。继续在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/course/目录并在其中建立detail.html和list.html两个文件。编辑 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,但是如果在其他页面登录,会被导向内置的account/profile/目录,所以修改settings.py增加登录成功的跳转URL设置:

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

这是用于让学生具体学习已选课程内容的页面。和之前CMS类似,建立一个列表放着所有课程的module;然后在右侧根据选中的module,使用了一个{{ item.render }}来展示所有的内容。

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

渲染不同种类的内容

有四种不同种类的内容,我们想为内容编写一个统一的渲染方式。编辑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

编辑text.html:

{{ item.content|linebreaks|safe }}

编辑file.html:

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

编辑image.html:

<p><img src="{{ item.file.url }}"></p>

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

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

回忆一下,这里MEDIA_URL是指上传媒体文件的路径,MEDIA_ROOT是指的查找媒体文件的时候开始的路径。由于是测试服务器才能如此配置,所以还必须配置项目根urls.py加入MEDIA_URL的内容:

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,之后被重定向到课程列表,应该可以看到类似下面的页面:

这样就完成了展示内容的通用方法。制作完了学生选课和听课的功能。我们的项目已经全面转向CBV,可以快速的编写具有各种功能和方法的视图,比使用FBV要更加灵活和易于扩展。

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

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

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

写死了选第一个,就会报错,修改方法是做个判断,如果长度=0,就返回空就可以了,这样页面不会渲染出内容。

使用缓存框架

从之前的项目一路做过来,可以发现几乎所有的视图对来自外部的HTTP请求都会进行数据查询和处理、渲染模板等工作,这比返回一个静态的页面开销要大很多。

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

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

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

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

关于详细的缓存机制,可以看官方文档

缓存后端

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

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

对于优化性能而言,最好选取基于内存缓存的缓存机制。这里就来使用Memcached服务。

安装Memcached服务

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

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

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

如果使用MacOS X安装了Homebrew,可以直接通过Homebrew下载。安装了Memcached之后,可以通过一个命令启动服务:

memcached -l 127.0.0.1:11211

如果源码编译安装还会需要安装libevent库。

此时Memcached就会在默认的11211端口运行。Centos 7系统也可以通过 systemctl start memcached启动Memcahed服务,配置可以到/etc/sysconfig/memcached进行修改。还可以通过 -l 来指定其他的主机和端口号。在安装了Memcached之后,还要安装Python的相关模块:

pip install python-memcached==1.59

Django的缓存设置

Django在settings.py中提供了下列设置:

CACHES 一个字典,包含当前项目所有可用的缓存
CACHE_MIDDLEWARE_ALIAS 缓存的别名
CACHE_MIDDLEWARE_KEY_PREFIX 缓存KEY的前缀,如果不同站点都用同一个Memcached服务,设置这个KEY避免发生键冲突
CACHE_MIDDLEWARE_SECONDS 缓存页面的时间

这其中CACHES是一个字典,其中包含一系列键与设置,详情如下:

BACKEND 缓存后端
KEY_FUNCTION 一个字符串,包含一个可调用函数的位置,这个函数接受前缀,版本和键名为参数,返回一个最终的cache key
KEY_PREFIX 设置给所有cache key的前缀
LOCATION 缓存的位置,由缓存后端觉得,可能是一个目录,一个主机+端口或者一个内存缓存的名称
OPTIONS 其他的向缓存后端传递的配置参数
TIMEOUT 过期设置,单位是秒。默认是300秒=5分钟,如果设置为None,则缓存键不会过期。
VERSION 缓存键的版本号,用于缓存版本控制

看到这里其实就明白了几分,缓存系统也是一个缓存键和值的系统。下边就给项目来增加缓存系统。

为项目增加Memcached缓存

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

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

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

监控Memcached服务

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

pip install django-memcache-status==1.3

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

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

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

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

缓存级别

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

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

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

使用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']

在Python shell里进行一些实验:

>>> 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'

如果你按照本书录入一段代码,再看书,再录入的话,此刻很可能shell中没有任何回应,这是因为20秒已经到了,该键就会从缓存中删除。可以重复执行一下存入和取出键,看看是否20秒内可以取出而超过20秒就无法取出。

注意,避免缓存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

在CourseListView的get()方法里,找到下面这一行:

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的统计页面,看看出现了哪些变化。

缓存动态数据

在之前的例子中,由于到了那个页面,固定要查询一次所有的Subject,所以将其缓存起来。但有的时候,想缓存基于动态生成的数据。这就必须建立动态的键,用于对应具体的缓存数据。看以下例子:

编辑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})

在这个视图里,之前是仅保存了全部Subject查询结果的缓存。现在把通过subject.id取到的课程也进行了缓存。但是由于按不同id取出来的课程不同,所以为不同的查询结果设置了动态生成的key。在每次获取查询结果之前,到缓存中去搜索,找不到就进行查询然后动态生成KEY存入缓存中。

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

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

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

缓存模板片段

缓存模板片段是比较高级别的缓存,需要在模板中加载缓存标签:{% 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变量传给前边的module_contents中的module,这样就建立了独特的键避免重复。

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

缓存视图

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

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

from django.views.decorators.cache import cache_page
然后把cache_page用于student_course_detail和student_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来构建cache key,对同一个视图函数,来自不同URL路由的结果,会被分别缓存。

缓存站点

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

要启用站点缓存,需要编辑settings.py,把UpdateCacheMiddleware和FetchFromCacheMiddleware中间件加入的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处理过的请求数据。关于中间件处理的详细顺序,请参考本站的Django进阶-中间件

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

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

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

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

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

总结

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

最后一个项目一改之前的风格,全部使用CBV来进行视图操作,大量练习和使用CBV,以及加深对Django内置模块的理解是学好最后一个项目的关键。

LICENSED UNDER CC BY-NC-SA 4.0
Comment