Representación y Caché del Contenido
En el post anterior, utilizamos la herencia de modelos y las relaciones genéricas para crear modelos de contenido de curso flexibles. Implementamos un campo de modelo personalizado y construimos un sistema de gestión de cursos utilizando vistas basadas en clases. Finalmente, creamos una funcionalidad de arrastrar y soltar JavaScript utilizando solicitudes HTTP asíncronas para ordenar los módulos del curso y su contenido.
En este capítulo, construirás la funcionalidad para acceder al contenido del curso, crear un sistema de registro de estudiantes y gestionar la inscripción de estudiantes en los cursos. También veremos cómo almacenar en caché datos utilizando el marco de trabajo de caché de Django.
En este post abarcaremos lo siguiente:
- Crear vistas públicas para mostrar información de cursos
- Construir un sistema de registro de estudiantes
- Gestionar la inscripción de estudiantes en cursos
- Representar contenido diverso para los módulos del curso
- Instalar y configurar Memcached
- Almacenar en caché contenido utilizando el marco de trabajo de caché de Django
- Utilizar los backends de caché de Memcached y Redis
- Monitorear tu servidor Redis en el sitio de administración de Django
Comencemos creando un catálogo de cursos para que los estudiantes puedan buscar cursos existentes e inscribirse en ellos.
Puedes encontrar el código fuente para este capítulo en https://github.com/PacktPublishing/Django-4-by-example/tree/main/Chapter14.
- Mostrando los Cursos
Para nuestro catálogo de cursos, necesitaremos implementar las siguientes funcionalidades:
- Listar todos los cursos disponibles, opcionalmente filtrados por materia.
- Mostrar una visión general de un solo curso.
Para lograr esto, edita el archivo views.py de la aplicación de cursos y agrega el siguiente código:
Elearning/educa/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})
Esta es la vista CourseListView. Hereda de TemplateResponseMixin y View. En esta vista, realizas las siguientes tareas:
1. Recuperas todas las materias, utilizando el método annotate() del ORM con la función de agregación Count() para incluir el número total de cursos para cada materia.
2. Recuperas todos los cursos disponibles, incluyendo el número total de módulos contenidos en cada curso.
3. Si se proporciona un parámetro de URL slug de materia, recuperas el objeto de materia correspondiente y limitas la consulta a los cursos que pertenecen a la materia dada.
4. Utilizas el método render_to_response() proporcionado por TemplateResponseMixin para renderizar los objetos en una plantilla y devolver una respuesta HTTP.
Ahora, creemos una vista detallada para mostrar una visión general de un solo curso. Agrega el siguiente código al archivo views.py:
Elearning/educa/courses/views.py
from django.views.generic.detail import DetailView
class CourseDetailView(DetailView):
model = Course
template_name = 'courses/course/detail.html'
Elearning/educa/educa/urls.py
from courses.views import CourseListView urlpatterns = [ # ... path('', CourseListView.as_view(), name='course_list'), ]
Añadimos el patrón de URL `course_list` al archivo principal urls.py del proyecto porque queremos mostrar la lista de cursos en la URL http://127.0.0.1:8000/, y todas las demás URLs para la aplicación de cursos tienen el prefijo /course/.
Edita el archivo urls.py de la aplicación courses y añade los siguientes patrones de URL:
Elearning/educa/courses/urls.py
path('subject/<slug:subject>/', views.CourseListView.as_view(), name='course_list_subject'), path('<slug:slug>/', views.CourseDetailView.as_view(), name='course_detail'),
Defines los siguientes patrones de URL:
- course_list_subject: Para mostrar todos los cursos para una materia.
- course_detail: Para mostrar una vista general de un solo curso.
Ahora, construyamos las plantillas para las vistas CourseListView y CourseDetailView.
Crea la siguiente estructura de archivos dentro del directorio templates/courses/ de la aplicación courses:
```
courses/
course/
list.html
detail.html
```
Edita la plantilla courses/course/list.html de la aplicación courses y escribe el siguiente código:
Elearning/educa/courses/templates/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 }} course{{ s.total_courses|pluralize }} </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 }} </p> {% endwith %} {% endfor %} </div> {% endblock %}
Asegúrate de que ninguna etiqueta de plantilla se divida en múltiples líneas.
Esta es la plantilla para listar los cursos disponibles. Creamos una lista HTML para mostrar todos los objetos Subject y construyes un enlace a la URL course_list_subject para cada uno de ellos. También incluyes el número total de cursos para cada materia y utilizas el filtro de plantilla pluralize para añadir un sufijo plural a la palabra "curso" cuando el número es diferente de 1, para mostrar 0 cursos, 1 curso, 2 cursos, etc. Añades una clase HTML seleccionada para resaltar la materia actual si una materia está seleccionada. Iteras sobre cada objeto Course, mostrando el número total de módulos y el nombre del instructor.
Ejecuta el servidor de desarrollo y abre http://127.0.0.1:8000/ en tu navegador. Deberías ver una página similar a la siguiente:
La barra lateral izquierda contiene todas las materias, incluyendo el número total de cursos para cada una de ellas. Puedes hacer clic en cualquier materia para filtrar los cursos mostrados.
Edita la plantilla courses/course/detail.html y añade el siguiente código:
Elearning/educa/courses/templates/courses/course/detail.html
{% extends "base.html" %} {% block title %} {{ object.title }} {% endblock %} {% block content %} {% with subject=object.subject %} <h1> {{ object.title }} </h1> <div class="module"> <h2>Overview</h2> <p> <a href="{% url 'course_list_subject' subject.slug %}"> {{ subject.title }}</a>. {{ object.modules.count }} modules. Instructor: {{ object.owner }} </p> {{ object.overview|linebreaks }} </div> {% endwith %} {% endblock %}
En esta plantilla, se muestra la descripción general y los detalles de un solo curso. Abre http://127.0.0.1:8000/ en tu navegador y haz clic en uno de los cursos. Deberías ver una página con la siguiente estructura:
Añadiendo un registro para los estudiantes.
Para agregar el registro de estudiantes, primero necesitas crear una nueva aplicación en tu proyecto Django. Puedes hacerlo ejecutando el siguiente comando en tu terminal:
python manage.py startapp students
Esto creará una nueva aplicación llamada "students" en tu proyecto Django. Una vez que la aplicación se haya creado, podrás comenzar a agregar las vistas, modelos y plantillas necesarias para manejar el registro de estudiantes y la inscripción en cursos.
Edita el archivo settings.py del proyecto educa y registra la aplicación de la siguiente forma:
/home/chema/PycharmProjects/Elearning/educa/educa/settings.py
INSTALLED_APPS = [ # ... 'students.apps.StudentsConfig', ]
Crear una vista para el registro de los estudiantes.
Edita el archivo views.py de la aplicación students y añade el siguiente código.
Elearning/educa/students/views.py
from django.shortcuts import render 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 # Create your views here. 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['password1']) login(self.request, user) return result
Esta es la vista que permite a los estudiantes registrarse en tu web. Utilizamos el módulo genérico CreateView, el cual nos proporciona la funcionalidad para crear modelos de objetos. Esta vista necesita los siguientes atributos:
- template_name - la ruta de la plantilla que renderizará esta vista.
- form_class - el formulario para crear los objetos, el cual tiene que heradar de ModelForm. Usaremos UserCreationForm como el formulario de registro para crear los objetos.
- success_url - la URL a la que redirigir al usuario cuando el formulario haya sido enviado exitosamente. Para ello utilizaremos 'student_course_list' que construiremos en breve para mostrar los cursos en los que está apuntado un estudiante.
Crea un nuevo archivo dentro de la aplicación students llamado urls.py. Añadele el siguiente código:
Elearning/educa/students/urls.py
from django.urls import path from . import views urlpatterns = [ path('register/', views.StudentRegistrationView.as_view(), name='student_registration'), ]
A continuación edita el archivo urls.py de la aplicación principal para registrar este patrón URLs de la siguiente manera.
Elearning/educa/educa/urls.py
urlpatterns = [ # ... path('students/', include('students.urls')), ]
Crea la siguiente estructura de directorios dentro de la aplicación students.
templates /
students /
student/
registration.html
Edita este último archivo creado y añade el siguiente código:
Elearning/educa/students/templates/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 method="post"> {{ form.as_p }} {% csrf_token %} <p><input type="submit" value="Create my account"></p> </form> </div> {% endblock %}
Ejecuta el servidor de desarrollo y abre la siguiente dirección http://127.0.0.1:8000/students/register/ en el navegador. Deberías ver un formulario de registro como el que se ve a continuación.
Ten en cuenta que el URL student_course_list especificado en al atributo success_url de la vista StudentRegistrationView aun no existe. Si enviaste el formulario, Django no podrá encontrar la URL a la que redirigirte después de registrarte con éxito. (La crearemos más adelante)
Apuntándose a los cursos.
Elearning/educa/courses/models.py
students = models.ManyToManyField(User, related_name='courses_joined', blank=True)
Desde el shell ejecuta el siguiente comando para realizar la migración de este cambio:
python manage.py makemigrations
y luego ejecuta la migración con:
python manage.py migrate
Ahora podemos asociar cada estudiante con el curso al que se haya apuntado. Creemos el código para que los usuarios puedan apuntarse a los cursos.
Crea un nuevo archivo dentro de la aplicación students y llámalo forms.py. Añade el siguiente código:
Elearning/educa/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)
Vamos a usar este formulario para que los estudiantes se puedan apuntar a los cursos. El campo course se usará para seleccionar el curso que quiere apuntarse el usuario. Por tanto tiene que ser un campo ModelChoiceField. Usaremos un widget HiddenInput porque no mostraremos este campo al usuario. Usaremos este formulario en la vista CourseDetailView que mostrará un botón para apuntarse al curso.
Edita el archivo views.py de la aplicación students y añade el siguiente código.
Elearning/educa/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().form_valid(form)
def get_success_url(self):
return reverse_lazy('student_course_detail', args=[self.course.id])
Esta es la vista StudentEnrollCourseView. Manejará el proceso de que los estudiantes se apunten al curso deseado. La vista hereda de LoginRequiredMixin por lo que solamente los usuarios que estén logueados pueden acceder a la vista. También hereda de la vista FormView ya que maneja el envío del formulario. También usamos el formulario CourseEnrollForm para el atributo form_class y también el atributo course para guardar posteriormente el objeto Course. Cuando el formulario es válido, se añade el usuario actual a los estudiantes apuntados al curso.
El método get_success_url() devuelve la URL a la que se redirigirá al usuario si el formulario fue enviado exitosamente. Este método es equivalente al atributo success_url. Luego, inviertes la URL llamada student_course_detail.
Edita el archivo urls.py de la aplicación students y añade el siguiente patrón URLs:
Elearning/educa/students/urls.py
path('enroll-course/',
views.StudentEnrollCourseView.as_view(),
name='student_enroll_course'),
Agreguemos el formulario del botón de inscripción a la página de descripción general del curso. Edita el archivo views.py de la aplicación courses y modifica CourseDetailView para que tenga el siguiente aspecto:
Elearning/educa/courses/views.py
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
Utilizamos el método get_context_data() para incluir el formulario de inscripción en el contexto para la representación de las plantillas. Inicializamos el campo de curso oculto del formulario con el objeto de curso actual para que se puede enviar directamente.
Edita la plantilla courses/course/detail.html y localiza la siguiente línea:
{{ object.overview|linebreaks }}
Remplázala con el siguiente código:
Elearning/educa/courses/templates/courses/course/detail.html
{{ object.overview|linebreaks }} {% if request.user.is_authenticated %} <form action="{% url " student_enroll_course" %}" method="post"> {{ enroll_form }} {% csrf_token %} <input type="submit" value="Enroll now"> </form> {% else %} <a href="{% url 'student_registration' %}" class="button"> Register to enroll </a>
Este código crea el botón para la inscripción en los cursos. Si el usuario está autentificado, se mostrará el botón de inscripción, incluyendo el formulario oculto que apunta a la URL student_enroll_course. Si no lo está, se mostrará un link para registrarte en la plataforma.
Asegúrate de que tienes en funcionamiento el servidor de desarrollo, abre la dirección http://127.0.0.1:8000/ en el navegador y haz clic en un curso. Si te has logueado verás el botón ENROLL NOW situado justo debajo de la descripción del curso, como puedes ver en la imagen siguiente:
Si no estás logueado verás el botón REGISTER TO ENROLL en su lugar.
Accediendo al contenido de los cursos.
Elearning/educa/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])
Continuamos añadiendo más código al mismo archivo:
Elearning/educa/students/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
Elearning/educa/students/views.py
path('courses/',
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'),
Ahora crea la siguiente estructura de archivos dentro del directorio templates/students/ de la aplicación students:
course/
detail.html
list.html
Edita la plantilla students/course/list.html y añade el siguiente código:
Elearning/educa/students/templates/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 on a course. </p> {% endfor %} </div> {% endblock %}
Este modelo muestra los cursos en los que el estudiante está inscrito. Recuerda que cuando un nuevo estudiante se registre exitosamente en la plataforma, será redirigido a la URL student_course_list. También redireccionemos a los estudiantes a esta URL cuando inicien sesión en la plataforma.
Edita el archivo settings.py del proyecto educa y añade el siguiente código:
Elearning/educa/educa/settings.py
from django.urls import reverse_lazy
LOGIN_REDIRECT_URL = reverse_lazy('student_course_list')
Este es el ajuste utilizado por el módulo de autenticación para redirigir al estudiante después de iniciar sesión correctamente si no hay ningún parámetro "next" presente en la solicitud. Después de iniciar sesión correctamente, un estudiante será redirigido a la URL student_course_list para ver los cursos en los que está inscrito.
Edita la plantilla students/course/detail.html y añade el siguiente código a ella:
Elearning/educa/students/templates/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 %}
Representar diferentes tipos de contenido.
Elearning/educa/courses/models.py
from django.template.loader import render_to_string class ItemBase(models.Model): # ... def render(self): return render_to_string( f'courses/content/{self._meta.model_name}.html', {'item': self})
También debemos crear una plantilla para representar objetos de Video. Utilizaremos django-embed-video para incrustar contenido de video. django-embed-video es una aplicación de Django de terceros que te permite incrustar videos en tus plantillas, desde fuentes como YouTube o Vimeo, simplemente proporcionando su URL pública.
Instala el paquete con el siguiente comando:
pip install django-embed-video
Como siempre que se instala una aplicación hay que registrarle, para ello edita el archivo settings.py del proyecto y añade lo siguiente:
Elearning/educa/educa/settings.py
INSTALLED_APPS = [ # ... 'embed_video', ]
Si quieres más información puedes encontrar la documentación de la aplicación en https://django-embed-video.readthedocs.io/en/latest/
Edita la plantilla courses/content/video.html y escribe el siguiente código:
{% load embed_video_tags %}
{% video item.url "small" %}
Esta es la plantilla para renderizar videos.
Ahora, ejecuta el servidor de desarrollo y accede a http://127.0.0.1:8000/course/mine/ en tu navegador. Accede al sitio con un usuario que pertenezca al grupo de Instructores, y añade varios contenidos a un curso. Para incluir contenido de video, simplemente copia cualquier URL de YouTube, como https://www.youtube.com/watch?v=bgV39DlmZ2U, y inclúyela en el campo de la URL del formulario.
Después de añadir contenido al curso, abre http://127.0.0.1:8000/, haz clic en el curso y luego haz clic en el botón ENROLL NOW (Inscribirse ahora). Deberías estar inscrito en el curso y ser redirigido a la URL del detalle del curso para estudiantes. La Figura 14.5 muestra una página de ejemplo de contenidos de curso:
Y con esto ya tenemos una interfaz para mostrar diferentes tipos de contenido.
Utilizando el marco de caché
El procesamiento de solicitudes HTTP a tu aplicación web generalmente implica acceso a la base de datos, manipulación de datos y renderizado de plantillas. Es mucho más costoso en términos de procesamiento que simplemente servir un sitio web estático. El sobrecosto en algunas solicitudes puede ser significativo cuando tu sitio comienza a recibir más tráfico. Aquí es donde la caché se vuelve valiosa. Al almacenar en caché consultas, resultados de cálculos o contenido renderizado en una solicitud HTTP, evitarás operaciones costosas en términos de proceso en las siguientes solicitudes que necesitan devolver los mismos datos. Esto se traduce en tiempos de respuesta más cortos y menos procesamiento en el lado del servidor.
Django incluye un sistema de caché robusto que te permite almacenar en caché datos con diferentes niveles de granularidad. Puedes almacenar en caché una única consulta, la salida de una vista específica, partes del contenido de la plantilla renderizada o todo tu sitio. Los elementos se almacenan en el sistema de caché durante un tiempo predeterminado, pero puedes especificar el tiempo de espera al almacenar en caché los datos.
Así es como normalmente utilizarás el marco de caché cuando tu aplicación procese una solicitud HTTP:
1. Intenta encontrar los datos solicitados en la caché.
2. Si se encuentra, devuelve los datos almacenados en caché.
3. Si no se encuentra, realiza los siguientes pasos:
1. Realiza la consulta a la base de datos o el procesamiento necesario para generar los datos.
2. Guarda los datos generados en la caché.
3. Devuelve los datos.
Puedes obtener información detallada sobre el sistema de caché de Django en https://docs.djangoproject.com/en/5.0/topics/cache/.
Backends de caché disponibles
Django viene con los siguientes backends de caché:
- backends.memcached.PyMemcacheCache o backends.memcached.PyLibMCCache: Backends de Memcached. Memcached es un servidor de caché basado en memoria rápida y eficiente. El backend a utilizar depende de las bibliotecas de enlace de Memcached que elijas.
- backends.redis.RedisCache: Un backend de caché de Redis. Este backend se ha añadido en Django 4.0.
- backends.db.DatabaseCache: Utiliza la base de datos como sistema de caché.
- backends.filebased.FileBasedCache: Utiliza el sistema de almacenamiento de archivos. Esto serializa y almacena cada valor de caché como un archivo separado.
- backends.locmem.LocMemCache: Un backend de caché de memoria local. Este es el backend de caché predeterminado.
- backends.dummy.DummyCache: Un backend de caché falso destinado únicamente para desarrollo. Implementa la interfaz de caché sin almacenar realmente nada en él. Esta caché es por proceso y segura para hilos.
Nota: para un resultado óptimo, usa el cache basado en memoria como Redis o Memcached.
Instalando Memcached.
Instalación de la imagen Docker de Memcached
sudo docker pull memcached
Esto descargará la imagen Docker de Memcached en tu máquina local. Si no deseas utilizar Docker, también puedes descargar Memcached desde https://memcached.org/downloads.
Ejecuta el contenedor Docker de Memcached con el siguiente comando de bash:
sudo docker run -it --rm --name memcached -p 11211:11211 memcached -m 64
Memcached se ejecuta por defecto en el puerto 11211. La opción -p se utiliza para publicar el puerto 11211 en la misma interfaz de host. La opción -m se utiliza para limitar la memoria del contenedor a 64 MB. Memcached se ejecuta en memoria y se le asigna una cantidad específica de RAM. Cuando la RAM asignada está llena, Memcached comienza a eliminar los datos más antiguos para almacenar nuevos datos. Si deseas ejecutar el comando en modo desvinculado (en segundo plano de tu terminal), puedes utilizar la opción -d.
Puedes encontrar más información sobre Memcached en https://memcached.org.
Instalación del enlace de Memcached para Python
Después de instalar Memcached, debes instalar un enlace de Memcached para Python. Vamos a instalar pymemcache, que es un cliente Memcached rápido y puro en Python. Ejecuta el siguiente comando en la terminal de bash:
pip install pymemcache
Puedes obtener más información sobre la biblioteca pymemcache en https://github.com/pinterest/pymemcache.
Configuración de la caché en Django
Django proporciona la siguiente configuración de caché:
- CACHES: Un diccionario que contiene todas las cachés disponibles para el proyecto.
- CACHE_MIDDLEWARE_ALIAS: El alias de caché a utilizar para el almacenamiento.
- CACHE_MIDDLEWARE_KEY_PREFIX: El prefijo a utilizar para las claves de caché. Establece un prefijo para evitar colisiones de claves si compartes la misma caché entre varios sitios.
- CACHE_MIDDLEWARE_SECONDS: El número predeterminado de segundos para almacenar en caché las páginas.
El sistema de caché para el proyecto se puede configurar utilizando la configuración CACHES. Esta configuración te permite especificar la configuración para múltiples cachés. Cada caché incluida en el diccionario CACHES puede especificar los siguientes datos:
- BACKEND: El backend de caché a utilizar.
- KEY_FUNCTION: Una cadena que contiene una ruta punteada a un llamable que toma un prefijo, versión y clave como argumentos y devuelve una clave de caché final.
- KEY_PREFIX: Un prefijo de cadena para todas las claves de caché, para evitar colisiones.
- LOCATION: La ubicación de la caché. Dependiendo del backend de caché, esto podría ser un directorio, un host y un puerto, o un nombre para el backend en memoria.
- OPTIONS: Cualquier parámetro adicional que se deba pasar al backend de caché.
- TIMEOUT: El tiempo de espera predeterminado, en segundos, para almacenar las claves de caché. Por defecto, son 300 segundos, que equivalen a 5 minutos. Si se establece en None, las claves de caché no caducarán.
- VERSION: El número de versión predeterminado para las claves de caché. Útil para el versionado de caché.
Agregando Memcached a tu proyecto
Vamos a configurar la caché para tu proyecto. Edita el archivo settings.py del proyecto educa y agrega el siguiente código a él:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache', 'LOCATION': '127.0.0.1:11211',
}
}
Estamos utilizando el backend PyMemcacheCache. Especificamos su ubicación utilizando la notación dirección:puerto. Si tienes múltiples instancias de Memcached, puedes usar una lista para LOCATION.
Hemos configurado Memcached para nuestro proyecto. ¡Comencemos a cachear datos!
Niveles de caché
Django proporciona los siguientes niveles de caché, enumerados aquí en orden ascendente de granularidad:
- API de caché de bajo nivel: Proporciona la mayor granularidad. Te permite cachear consultas específicas o cálculos.
- Caché de plantilla: Te permite cachear fragmentos de plantilla.
- Caché por vista: Proporciona caché para vistas individuales.
- Caché por sitio: La caché de nivel más alto. Cachéa todo tu sitio.
Antes de implementar la caché, es importante pensar en tu estrategia de caché. Enfócate primero en las consultas o cálculos costosos que no se calculan de manera individual para cada usuario.
Algunas consideraciones importantes para tu estrategia de caché pueden incluir:
1. **Identificar consultas costosas**: Revisa tu código para identificar consultas de base de datos u operaciones computacionales que consumen muchos recursos y que no cambian con frecuencia.
2. **Análisis de patrones de acceso**: Observa cómo los usuarios interactúan con tu aplicación y determina qué datos o páginas se acceden con más frecuencia.
3. **Tiempo de vida de la caché**: Decide cuánto tiempo deseas mantener los datos en la caché antes de que se consideren obsoletos. Esto puede variar según el tipo de datos y la frecuencia de actualización.
4. **Tamaño de la caché**: Considera cuánta memoria o espacio en disco estás dispuesto a asignar para la caché y ajusta en consecuencia.
5. **Invalidación de caché**: Implementa un mecanismo para invalidar la caché cuando los datos subyacentes cambien. Esto asegura que los usuarios siempre vean información actualizada.
6. **Monitoreo y ajuste**: Realiza un seguimiento del rendimiento de la caché y ajusta tu estrategia según sea necesario. Es posible que necesites modificar los tiempos de vida de la caché o la granularidad de la caché según la evolución de tu aplicación.
Comencemos aprendiendo cómo utilizar la API de caché de bajo nivel en tu código Python.
Uso de la API de caché de bajo nivel
La API de caché de bajo nivel te permite almacenar objetos en la caché con cualquier granularidad. Se encuentra en django.core.cache. Puedes importarlo de la siguiente manera:from django.core.cache import cache
Piensa en tu estrategia de caché antes de implementarla. Enfócate primero en consultas o cálculos costosos que no se calculan a nivel de usuario.
Esto utiliza la caché predeterminada. Es equivalente a caches['default']. También es posible acceder a una caché específica a través de su alias.
Vamos a echar un vistazo a cómo funciona la API de caché. Abre la consola de Django con el siguiente comando:
python manage.py shell
Ejecuta el siguiente código:
>>> from django.core.cache import cache
>>> cache.set('musician', 'Django Reinhardt', 20)
Esto accede al backend de caché predeterminado y utiliza set(key, value, timeout) para almacenar una clave llamada 'musician' con un valor que es la cadena 'Django Reinhardt' durante 20 segundos. Si no especificas un tiempo de espera, Django utiliza el tiempo de espera predeterminado especificado para el backend de caché en la configuración CACHES. Ahora, ejecuta el siguiente código:
>>> cache.get('musician')
'Django Reinhardt'
Recuperas la clave de la caché. Espera 20 segundos y ejecuta el mismo código nuevamente:
>>> cache.get('musician')
Esta vez no se devuelve ningún valor. La clave de caché 'musician' ha caducado y el método get() devuelve None porque la clave ya no está en la caché.
NOTA: Siempre evita almacenar un valor None en una clave de caché porque no podrás distinguir entre el valor real y una falta de coincidencia en la caché.
Vamos a cachear un QuerySet con el siguiente código:
>>> from courses.models import Subject
>>> subjects = Subject.objects.all()
>>> cache.set('my_subjects', subjects)
Realizas una consulta QuerySet en el modelo Subject y almacenas los objetos devueltos en la clave 'my_subjects'. Ahora, vamos a recuperar los datos en caché:
>>> cache.get('my_subjects')
<QuerySet [<Subject: Matemáticas>, <Subject: Música>, <Subject: Física>,
<Subject: Programación>]>
Ahora, vas a cachear algunas consultas en tus vistas. Edita el archivo views.py de la aplicación courses y agrega la siguiente importación:
from django.core.cache import cache
En el método get() de CourseListView, encuentra las siguientes líneas:
subjects = Subject.objects.annotate(
total_courses=Count('courses'))
Reemplaza esas líneas con las siguientes:
subjects = cache.get('all_subjects')
if not subjects:
subjects = Subject.objects.annotate(
total_courses=Count('courses'))
cache.set('all_subjects', subjects)
En este código, intentas obtener la clave all_subjects de la caché utilizando cache.get(). Esto devuelve None si la clave dada no se encuentra. Si no se encuentra ninguna clave (aún no está en caché o estaba en caché pero ha expirado), realizas la consulta para recuperar todos los objetos de Subject y su número de cursos, y luego cachéas el resultado usando cache.set().
Comprobando las peticiones al cache con Django Debug Toolbar.
Vamos a añadir esta herramienta al proyecto para comprobar el funcionamiento de las peticiones al caché. Si recuerdas ya vimos como se usaba en el post 21.- Barra de Herramientas de Depuración de Django.
Primeramente lo instalaremos con:
pip install django-debug-toolbar
Luego, como cada vez que se instala una aplicación nueva, la registraremos en la aplicación dentro del archivo settings.py del proyecto:
Elearning/educa/educa/settings.py
INSTALLED_APPS = [ # ... 'debug_toolbar', ]
y en el mismo archivo añade el código resaltado en azul dentro del apartado MIDDELWARE.
Elearning/educa/educa/settings.py
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
Recuerda que DebugToolbarMiddleware debe colocarse antes que cualquier otro middleware, excepto el middleware que codifica el contenido de la respuesta, como GZipMiddleware, que, si está presente, debe colocarse primero.
Agrega las siguientes líneas al final del archivo settings.py:
INTERNAL_IPS = [
'127.0.0.1',
]
Django Debug Toolbar solo se mostrará si tu dirección IP coincide con una entrada en la configuración INTERNAL_IPS.
Edita el archivo urls.py principal del proyecto y agrega el siguiente patrón de URL a urlpatterns:
path('__debug__/', include('debug_toolbar.urls')),
Ejecuta el servidor de desarrollo y abre http://127.0.0.1:8000/ en tu navegador.
Ahora deberías ver la barra de herramientas de depuración de Django en el lado derecho de la página. (Recuerda tener activado el docker de Memcached porque sino no djagno te mostrará un error). Haz clic en "Cache" en el menú lateral. Verás el siguiente panel:
Debajo de "Total calls" deberías ver 2. La primera vez que se ejecuta la vista CourseListView hay dos solicitudes de caché. Bajo "Commands" verás que el comando "get" se ha ejecutado una vez, y que el comando "set" también se ha ejecutado una vez. El comando "get" corresponde a la llamada que recupera la clave de caché "all_subjects". Esta es la primera llamada que se muestra en "Calls". La primera vez que se ejecuta la vista, ocurre un fallo de caché porque aún no hay datos en caché. Por eso hay un 1 bajo "Cache misses". Luego, el comando "set" se utiliza para almacenar los resultados del conjunto de consultas "subjects" en la caché utilizando la clave de caché "all_subjects". Esta es la segunda llamada que se muestra en "Calls".
En el menú "SQL" de Django Debug Toolbar, verás el número total de consultas SQL ejecutadas en esta solicitud. Esto incluye la consulta para recuperar todos los temas que luego se almacenan en la caché.
Recarga la página en el navegador y haz clic en "Cache" en el menú lateral:
Ahora, solo hay una solicitud de caché. Debajo de "Total calls" deberías ver 1. Y bajo "Commands" puedes ver que la solicitud de caché corresponde a un comando "get". En este caso, hay un acierto de caché (ver "Cache hits") en lugar de un fallo de caché porque los datos se han encontrado en la caché. Bajo "Calls" puedes ver la solicitud "get" para recuperar la clave de caché "all_subjects".
Consulta el elemento de menú "SQL" de la barra de herramientas de depuración. Deberías ver que hay una consulta SQL menos en esta solicitud. Estás ahorrando una consulta SQL porque la vista encuentra los datos en la caché y no necesita recuperarlos de la base de datos:
En este ejemplo, para una solicitud única, lleva más tiempo recuperar el elemento de la caché que el tiempo ahorrado en la consulta SQL adicional. Sin embargo, cuando tienes muchos usuarios accediendo a tu sitio, encontrarás reducciones significativas de tiempo al recuperar los datos de la caché en lugar de acceder a la base de datos, y podrás atender el sitio a más usuarios concurrentes.
Las solicitudes sucesivas a la misma URL recuperarán los datos de la caché. Dado que no especificamos un tiempo de espera al almacenar en caché los datos con `cache.set('all_subjects', subjects)` en la vista CourseListView, se utilizará el tiempo de espera predeterminado (300 segundos por defecto, que son 5 minutos). Cuando se alcanza el tiempo de espera, la siguiente solicitud a la URL generará un fallo de caché, se ejecutará el QuerySet y los datos se almacenarán en caché durante otros 5 minutos. Puedes definir un tiempo de espera predeterminado diferente en el elemento TIMEOUT de la configuración CACHES.
Caché basada en datos dinámicos
Edita el archivo views.py de la aplicación courses y modifica la vista CourseListView para que se vea así:
Elearning/educa/courses/views.py
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')) 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 = f'subject_{subject.id}_courses' 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})
En este caso, también se almacenan en caché tanto todos los cursos, como los cursos filtrados por materia. Se utiliza la clave de caché "all_courses" para almacenar todos los cursos si no se proporciona ninguna materia. Si hay una materia, se construye la clave dinámicamente con f'subject_{subject.id}_courses'.
Es importante tener en cuenta que no puedes usar un QuerySet en caché para construir otros QuerySets, ya que lo que has almacenado en caché son realmente los resultados del QuerySet. Por lo tanto, no puedes hacer lo siguiente:
courses = cache.get('all_courses')
courses.filter(subject=subject)
En su lugar, debes crear el QuerySet base Course.objects.annotate(total_modules=Count('modules'))
, que no se ejecutará hasta que se fuerce, y se use para restringir aún más el QuerySet con all_courses.filter(subject=subject)
en caso de que los datos no se encuentren en la caché.
Almacenamiento en cache de fragmentos de plantilla.
El almacenamiento en caché de fragmentos de plantilla es un enfoque de nivel superior. Necesitas cargar las etiquetas de caché de plantilla en tu plantilla usando {% load cache %}
. Luego, podrás usar la etiqueta de plantilla {% cache %}
para almacenar en caché fragmentos de plantilla específicos. Normalmente, usarás la etiqueta de plantilla de la siguiente manera:
{% cache 300 fragment_name %}
...
{% endcache %}
La etiqueta de plantilla {% cache %}
tiene dos argumentos requeridos: el tiempo de espera en segundos y un nombre para el fragmento. Si necesitas almacenar en caché contenido que depende de datos dinámicos, puedes hacerlo pasando argumentos adicionales a la etiqueta de plantilla {% cache %}
para identificar de manera única el fragmento.
Edita la plantilla de la aplicación students, /students/course/detail.html. Añade el siguiente código en la parte superior, justo después de la etiqueta {% extends %}
{% load cache %}
Luego encuentra las siguientes líneas:
{% with item=content.item %}
<h2>{{ item.title }}</h2>
{{ item.render }}
{% endwith %}
{% endfor %}
Reemplázalas con las siguientes:
Elearning/educa/courses/views.py
{% cache 600 module_contents module %} {% for content in module.contents.all %} {% with item=content.item %} <h2>{{ item.title }}</h2> {{ item.render }} {% endwith %} {% endfor %} {% endcache %}
Has cachéado este fragmento de plantilla utilizando el nombre `module_contents` y pasas el objeto module actual a él. De esta manera, identificas de forma única el fragmento. Esto es importante para evitar cachear el contenido de un módulo y servir el contenido incorrecto cuando se solicita un módulo diferente.
Si el ajuste USE_I18N está configurado como True, el caché del middleware para el sitio respetará el idioma activo. Si usas la etiqueta de plantilla {% cache %}, debes usar una de las variables específicas de traducción disponibles en las plantillas para lograr el mismo resultado, como {% cache 600 name request.LANGUAGE_CODE %}.
Caché de vistas
Puedes cachear la salida de vistas individuales utilizando el decorador cache_page ubicado en django.views.decorators.cache. El decorador requiere un argumento de tiempo de espera (en segundos).
Vamos a usarlo en las vistas. Edita el archivo urls.py de la aplicación students y agrega la siguiente importación y aplica el decorador cache_page a los patrones de URL student_course_detail y student_course_detail_module, de la siguiente manera:
Elearning/educa/students/urls.py
from django.views.decorators.cache import cache_page #... 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'),
Ahora, el contenido completo devuelto por StudentCourseDetailView se cacheará durante 15 minutos.
Nota: El caché por vista utiliza la URL para construir la clave de caché. Las múltiples URLs que apuntan a la misma vista se cachearán por separado. Esto significa que si tienes varias URLs que apuntan a la misma vista, cada una de ellas generará una entrada de caché independiente en función de su URL específica.
Usando de per-site cache
Para utilizar el caché por sitio, que es el nivel de caché más alto y te permite cachear todo tu sitio, sigue estos pasos:
1.- Edita el archivo settings.py de tu proyecto.
2.- Añade las clases UpdateCacheMiddleware y FetchFromCacheMiddleware al ajuste MIDDLEWARE, como se muestra a continuación:
Elearning/educa/educa/settings.py
MIDDLEWARE = [ 'debug_toolbar.middleware.DebugToolbarMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.cache.UpdateCacheMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.cache.FetchFromCacheMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ]
Recuerda que el middleware se ejecuta en el orden dado durante la fase de solicitud y en orden inverso durante la fase de respuesta. UpdateCacheMiddleware se coloca antes de CommonMiddleware porque se ejecuta durante el tiempo de respuesta, cuando el middleware se ejecuta en orden inverso. FetchFromCacheMiddleware se coloca después de CommonMiddleware intencionalmente porque necesita acceder a los datos de solicitud establecidos por este último.
A continuación, agrega las siguientes configuraciones al archivo settings.py:
CACHE_MIDDLEWARE_ALIAS = 'default'
CACHE_MIDDLEWARE_SECONDS = 60 * 15 # 15 minutos
CACHE_MIDDLEWARE_KEY_PREFIX = 'educa'
En estas configuraciones, utilizas la caché predeterminada para tu middleware de caché y estableces el tiempo de espera global de la caché en 15 minutos. También especificas un prefijo para todas las claves de caché para evitar colisiones en caso de que utilices el mismo backend de Memcached para varios proyectos. Tu sitio ahora cacheará y devolverá contenido en caché para todas las solicitudes GET.
Puedes acceder a las diferentes páginas y verificar las solicitudes de caché utilizando Django Debug Toolbar. El caché por sitio no es viable para muchos sitios porque afecta a todas las vistas, incluso a aquellas que es posible que no desees cachear, como las vistas de gestión donde deseas que los datos se devuelvan desde la base de datos para reflejar los últimos cambios.
Para desactivar el caché por sitio en tu proyecto y mantener las vistas de gestión de contenido para instructores sin ningún caché, puedes comentar las clases UpdateCacheMiddleware y FetchFromCacheMiddleware en la configuración MIDDLEWARE de tu archivo settings.py. Aquí tienes cómo hacerlo:
MIDDLEWARE = [ 'debug_toolbar.middleware.DebugToolbarMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', # 'django.middleware.cache.UpdateCacheMiddleware', 'django.middleware.common.CommonMiddleware', # 'django.middleware.cache.FetchFromCacheMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ]
Comentar estas líneas desactivará el middleware de caché por sitio. Esto significa que las vistas que estaban siendo cacheadas ya no lo estarán. Sin embargo, las vistas que no estaban siendo cacheadas, como las vistas de gestión de contenido para instructores, seguirán sin estar en caché.
Usando de Redis cache backend.
Django 4.0 introdujo un backend de caché de Redis. Vamos a cambiar la configuración para utilizar Redis en lugar de Memcached como backend de caché para el proyecto. Recuerda que ya has utilizado Redis en el post 26, "Construyendo un motor de recomendaciones de productos".
Instala `redis-py` en tu entorno utilizando el siguiente comando:
pip install redis
Luego, edita el archivo `settings.py` del proyecto `educa` y modifica la configuración `CACHES`, de la siguiente manera:
Elearning/educa/educa/settings.py
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379',
}
}
El proyecto ahora utilizará RedisCache como backend de caché. La ubicación se define en el formato redis:// [host]:[puerto]. Utilizas 127.0.0.1 para apuntar al host local y 6379, que es el puerto predeterminado para Redis. Inicializa el contenedor de Docker de Redis usando el siguiente comando:
docker run -it --rm --name redis -p 6379:6379 redis
Si deseas ejecutar el comando en segundo plano (en modo desacoplado), puedes usar la opción -d.
Ejecuta el servidor de desarrollo y abre http://127.0.0.1:8000/ en tu navegador. Verifica las solicitudes de caché en el panel de Cache de Django Debug Toolbar. Ahora estás utilizando Redis como backend de caché de tu proyecto en lugar de Memcached.
Monitoreo de Redis con Django Redisboard
Instala django-redisboard en tu entorno utilizando el siguiente comando: pip install django-redisboard Instala la biblioteca Python attrs utilizada por django-redisboard en tu entorno con el siguiente comando: pip install attrs Edita el archivo settings.py de tu proyecto y añade la aplicación al ajuste INSTALLED_APPS, de la siguiente manera:
INSTALLED_APPS = [ # ... 'redisboard', ]
Ejecuta el siguiente comando desde el directorio de tu proyecto para ejecutar las migraciones de Django Redisboard:
python manage.py migrate redisboard
Inicia el servidor de desarrollo y abre http://127.0.0.1:8000/admin/redisboard/redisserver/add/ en tu navegador para agregar un servidor Redis para monitorear. En el campo "Etiqueta", ingresa "redis", y en el campo "URL", ingresa redis://localhost:6379/0, como se muestra en la Figura 14.10:
Vamos a monitorear la instancia de Redis que se está ejecutando en nuestro host local, la cual utiliza el puerto 6379 y la base de datos Redis numerada como 0. Haz clic en GUARDAR. La información se guardará en la base de datos y podrás ver la configuración y las métricas de Redis en el sitio de administración de Django.
Puedes encontrar el código del capitulo en este enlace de GITHUB.