在上一章中,使用了模型继承和通用关系建立弹性的课程、章节和内容的关联数据模型,并且建立了一个CMS系统,在其中使用了CBV,表单集和AJAX管理课程内容。在这一章将要做的事情是:
我们就从建立一个课程目录,供学生们浏览和选课来开始本章。
为了展示课程,我们需要实现如下功能:
由于数据模型已经齐备,编辑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
视图,执行了如下任务:
annotate()
分组和Count()
聚合方法生成一个其中包含课程的数量字段。slug
字段,就取得该slug
对应的具体主题,并且将课程设置为该主题对应的课程,而不是全部课程。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'),
我们添加了如下两条路由:
course_list_subject
:展示所有的或某个主题下的课程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/,点击右侧任意一个课程,可以看到如下页面:
我们已经建立好了公共的(不需要特别权限)的展示课程的页面,下一步,需要允许用户以学生身份注册并且选课。
建立一个新的应用来管理学生注册功能:
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
这是允许学生注册的视图,继承了内置的CreateView
视图,该视图提供了创建模型对象的一般方法。这个视图需要如下属性:
template_name
:需要渲染的模板位置form_class
:必须是一个ModelForm
对象,这里指定为Django内置的建立新用户的UserCreationForm
表单。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。
在用户注册成功之后,应该能够让其选课以便加入到某门课的学习中去。很显然,一个学生可以选择多门课程,一个课程也有多个学生,需要在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的按钮。
在学生选好课之后,还必须创建一个视图给学生展示课程中的章节和内容,以便让他们访问课程内容来进行具体学习。
编辑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()方法还没有编写,在下一节中就来为每个内容对象编写这个方法来展示不同种类的内容。
我们想为不同的内容编写一个统一的渲染方式。编辑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>
由于ImageField
和FileField
都是文件字段,为了管理这两个字段,必须在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,就返回空就可以了,这样页面不会渲染出内容。
HTTP请求对我们的Web应用来说,意味着查询数据和处理业务逻辑和渲染模板等工作,这比返回一个静态的页面开销要大很多。
当站点的流量越来越大的时候,大量访问给后端带领的压力是巨大的。这个时候就是缓存系统大派用场的时刻。把一个HTTP请求导致的数据查询,业务逻辑处理结果,甚至渲染后的内容缓存起来,就可以避免在后续类似的请求中反复执行开销大的操作,会有效地提高网站的响应时间。
Django包含一个健壮的缓存系统,可以缓存不同粒度的数据。可以缓存一个查询,一个视图的返回结果,部分模板的渲染内容,甚至整个站点。在缓存系统中存储的数据有时效性,可以设置其过期的时间。
当应用接到一个HTTP请求的时候,通常按照如下的顺序使用缓存系统:
关于详细的缓存机制,可以官方文档:https://docs.djangoproject.com/en/2.0/topics/cache/。
就像数据库一样,Django的缓存机制可以使用多种缓存服务后端来完成,主要有这些:
backends.memcached.MemcachedCache
或backends.memcached.PyLibMCCache
:是基于Memcached服务的后端。具体使用哪种后端取决于采用哪种Python支持的Memcached模块。backends.db.DatabaseCache
:使用数据库作为缓存(还记得Redis吗)backends.filebased.FileBasedCache
:使用文件作为缓存,序列化每个缓存数据为一个单独的文件backends.locmem.LocMemCache
:本地内存缓存,这是默认值。backends.dummy.DummyCache
:伪缓存机制,仅用于开发。提供了缓存界面但实际上不缓存任何内容。每个进程的缓存互相独立而且线程安全。对于优化性能而言,最好选取基于内存缓存的缓存机制比如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
Django提供了下列缓存设置:
CACHES
:一个字典,包含当前项目所有可用的缓存CACHE_MIDDLEWARE_ALIAS
:缓存的别名CACHE_MIDDLEWARE_KEY_PREFIX
:缓存键名的前缀,如果不同站点都用同一个Memcached服务,设置这个KEY可以避免发生键冲突CACHE_MIDDLEWARE_SECONDS
:缓存页面的时间通过CACHES
可以设置项目的缓存系统。这个设置以字典的形式,可以配置多个缓存后端的设置。每个CACHES
中的缓存后端有如下设置:
BACKEND
:缓存后端KEY_FUNCTION
:生成键的函数,是一个字符串,包含一个可调用函数的位置,这个函数接受前缀,版本和键名为参数,返回一个最终的缓存键KEY_PREFIX
:缓存键名的前缀LOCATION
:缓存后端的位置,根据不同的后端配置,可能是一个目录,一个主机+端口或者一个内存缓存的名称OPTIONS
:其他的向缓存后端传递的配置参数TIMEOUT
:过期设置,单位是秒。默认是300秒=5分钟,如果设置为None,则缓存键不会过期。VERSION
:缓存键的版本号,用于缓存版本信息编辑settings.py
,将上述的缓存设置加入到文件中:
CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 'LOCATION': '127.0.0.1:11211', } }
这里指定了后端为Memcached
,然后指定了主机IP和端口号。如果有很多Memcached配置在不同主机上,可以给LOCATION
传一个列表。
为了监控Memcached的服务,可以使用第三方模块django-memcache-status
,该模块可以在管理后台中显示Memcached的统计情况。安装该模块:
pip install django-memcache-status==1.3
编辑setting.py
,激活该应用:
INSTALLED_APPS = [ # ... 'memcache_status', ]
确保Memcached服务在运行,然后进入站点的管理后台,可以看到如下的内容:
绿色的条代表空闲的缓存容量,红色代表已经使用的内容。如果点击标题,可以展开一个详情页看具体内容。已经为项目配置好了Memcached服务,现在来缓存具体内容。
Django为不同粒度的数据提供了如下的缓存级别:
在建立缓存系统之前,必须仔细考虑缓存策略。建议对不基于具体用户身份的,系统开销比较大的数据库查询和密集计算进行缓存
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
在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的统计页面,看看统计数据的变化。
但有的时候,想缓存动态生成的数据。这就必须建立动态的键,用于唯一确定对具体的缓存数据。看以下例子:
编辑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)
生成查询结果的原因。
缓存模板片段是比较高级别的缓存,需要在模板中加载缓存标签:{% 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 %}.
。
可以通过使用django.views.decorators.cache
中的cache_page
装饰器来缓存视图的输出结果,需要一个参数timeout
,是过期秒数。
在视图中使用该装饰器,编辑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来构建缓存键,对同一个视图函数,来自不同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
处理过的请求数据。
然后还需要把下列设置加入到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。