Создание сайта на Django: Урок 30, установка debug-toolbar, оптимизация SQL запросов (n + 1)
avatar
7 | (offline)
❤️‍🔥Notehunter Developer
Добавлено:
Категория: Руководства «Django»
Комментариев: 0

Установка Django Debug Toolbar

Для начала мы установим Django Debug Toolbar в наш Django блог проект с помощью команды в терминале: pip install django-debug-toolbar

Результат установки:

>>> Collecting django-debug-toolbar
>>> Downloading django_debug_toolbar-3.6.0-py3-none-any.whl (220 kB)
>>>     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 220.2/220.2 KB 1.3 MB/s eta 0:00:00
>>> Requirement already satisfied: sqlparse>=0.2.0 in c:\users\razilator\desktop\courses\app\venv\lib\site-packages (from django-debug-toolbar) (0.4.2)
>>> Requirement already satisfied: Django>=3.2.4 in c:\users\razilator\desktop\courses\app\venv\lib\site-packages (from django-debug-toolbar) (4.1)
>>> Requirement already satisfied: tzdata in c:\users\razilator\desktop\courses\app\venv\lib\site-packages (from Django>=3.2.4->django-debug-toolbar) (2022.2)
>>> Requirement already satisfied: asgiref<4,>=3.5.2 in c:\users\razilator\desktop\courses\app\venv\lib\site-packages (from Django>=3.2.4->django-debug-toolbar) (3.5.2)
>>> Installing collected packages: django-debug-toolbar
>>> Successfully installed django-debug-toolbar-3.6.0

Отлично, теперь нужно его подключить:

Убедитесь, что у вас установлена статика:

backend/settings.py

INSTALLED_APPS = [
    # ...
    "django.contrib.staticfiles",
    # ...
]

STATIC_URL = "static/"

Теперь добавим наш дебагер:

backend/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sites',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'mptt',
    'debug_toolbar',
    'modules.blog.apps.BlogConfig',
    'modules.system.apps.SystemConfig',
]

Также нам необходимо добавить middleware

backend/settings.py

MIDDLEWARE = [
    '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',
    'debug_toolbar.middleware.DebugToolbarMiddleware',
]

Также добавим возле SITE_ID = 1 в settings.py

backend/settings.py

INTERNAL_IPS = [
    '127.0.0.1',
]

И наконец в urls.py:

backend/urls.py

from django.contrib import admin
from django.urls import path, include

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('modules.blog.urls')),
    path('s/', include('modules.system.urls'))
]

if settings.DEBUG:
    urlpatterns = [path('__debug__/', include('debug_toolbar.urls'))] + urlpatterns
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Проверяем на сайте:

Как видите из скриншота выше, у нас на странице 8 запросов. Давайте их оптимизируем.

Для этого посмотрим SQL:

В Django существует ряд полезных встроенных функций, и одна из них select_related() позволяет нам точно указать Django, какие связанные модели мы хотим, чтобы он мог заранее выполнить JOINs.

В нашем случае, у нас есть модель Article, которая ссылается на Category в списке всех материалов. Давайте попробуем оптимизировать это.

Помните наш кастомный менеджер? ArticleManager() через него мы и будем оптимизировать! 

modules/blog/managers/articles.py

Напоминаю, что мы менеджер создали в уроке 17, свой кастомный менеджер QuerySet

from django.db import models


class ArticleManager(models.Manager):
    """
    Кастомный менеджер для модели статей.
    """

    def all(self):
        """
        Список статей (SQL запрос с фильтрацией для страницы списка статей)
        """
        return self.get_queryset().filter(is_published=True)

И так, давайте его дополним:

class ArticleManager(models.Manager):
    """
    Кастомный менеджер для модели статей.
    """

    def all(self):
        """
        Список статей (SQL запрос с фильтрацией для страницы списка статей)
        """
        return self.get_queryset().filter(is_published=True).select_related('category')

Отлично, давайте в нашем views/articles.py исправим кое-что если вы следовали всем урокам:

modules/blog/views/articles.py

class ArticleListView(ListView):
    model = Article
    template_name = 'modules/blog/articles/article-list.html'
    context_object_name = 'articles'
    paginate_by = 10
    queryset = Article.objects.all().filter(is_published=True)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['title'] = 'Главная страница'
        return context

 Нужно изменить queryset = Article.objects.all().filter(is_published=True) на queryset = Article.custom.all()

Кстати, оптимизировать мы также можем и в самом ListView в свойстве queryset, например сделать так:

class ArticleListView(ListView):
    model = Article
    template_name = 'modules/blog/articles/article-list.html'
    context_object_name = 'articles'
    paginate_by = 10
    queryset = Article.objects.all().filter(is_published=True).select_related('category')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['title'] = 'Главная страница'
        return context

Это будет аналогично тому, что вы делаете в кастомном менеджере, но ведь лучше хранить некоторые элементы по отдельности, чтоб их потом было легче найти из всей кучи, верно? Поэтому я и добавляю сразу это в наш кастомный менеджер. Давайте посмотрим на результат такой работы:

Отлично, для списка статей мы провели оптимизацию. А давайте сделаем тоже самое для полной странички статьи? Для примера я добавлю все ссылаемые элементы прям в шаблон:

templates/modules/blog/articles/article-detail.html

{% extends 'main.html' %}

{% block content %}
    <div class="card border-0">
        <div class="card-body">
            <div class="row">
                <div class="col-md-4">
                    <figure>
                        <img src="{{ article.get_thumbnail }}" width="200" alt="{{ article.title }}">
                    </figure>
                </div>
                <div class="col-md-8">
                     <h5 class="card-title">
                        {{ article.title }}
                    </h5>
                    <small class="card-subtitle">
                        {{ article.created_at }} / {{ article.category }}
                    </small>
                    <div class="card-text">
                        {{ article.full_description }}
                    </div>
                     <hr/>
                      Добавил: <img src="{{ article.author.profile.get_avatar }}" class="rounded-circle" width="26" height="26"/> {{ article.author }}
                    <hr/>
                    {% if article.reason %}{{ article.reason }} / обновил: {{ article.updated_by }}{% endif %}
                </div>
            </div>
        </div>
    </div>
{% endblock %}

Из структуры видно, что мы видим и автора и категорию и того кто обновил.

Теперь давайте оптимизируем в нашем менеджере:

modules/blog/managers/articles.py

class ArticleManager(models.Manager):
    """
    Кастомный менеджер для модели статей.
    """

    def all(self):
        """
        Список статей (SQL запрос с фильтрацией для страницы списка статей)
        """
        return self.get_queryset().filter(is_published=True).select_related('category')

    def detail(self):
        """
        Детальная страница статьи с оптимизацией
        """
        return self.get_queryset().filter(is_published=True).select_related('category', 'updated_by', 'author', 'author__profile')

Пояснения:

  • Я добавил метод detail в наш кастомный менеджер, который мы будем использовать при детальном просмотре странице. 
  • В join я завел категорию, апдейтера, автора, и его профиль. Из другой модели (author) ссылаемой на модель (profile) - необходимо использовать два __

Добавим наш метод в ArticleDetailView:

modules/blog/views/articles.py

class ArticleDetailView(DetailView):
    model = Article
    template_name = 'modules/blog/articles/article-detail.html'
    context_object_name = 'article'
    queryset = Article.custom.detail()

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['title'] = self.object.title
        return context

Наконец посмотрим на результат:

Из 8 запросов получилось 4 запроса, что является отличным результатом.

Есть ещё метод prefetch_related()

Мы используем prefetch_related, когда собираемся получить набор вещей. Это означает обработку ManyToMany и обратных ManyToMany, ForeignKey. prefetch_related выполняет отдельный поиск для каждой связи и выполняет «объединение» в Python. Он отличается от select_related. prefetch_related выполнял JOIN с использованием Python, а не в базе данных. Подробнее я вам расскажу об этом запросе когда мы создадим модель ссылающуюся на статью, в нашем случае это будут комментарии.

Подробнее о select_related() и prefetch_related()

Комментарии к статье 0
Комментариев нет
Форма добавления комментария (необходима регистрация)