Пять распространенных ошибок Django

Spread the love

Перевод: Steven PateFive Common Django Mistakes

Введение

Django – это фантастический фреймворк для создания веб-приложений. Когда вы только начинаете работать с Django, вы можете часто совершать одни и те же небольшие ошибки из-за недостатка знаний. Я написал этот пост чтобы помочь осветить некоторые часто встречаемые мною ошибки в чужом коде.

В этом посте мы рассмотрим часто встречаемые ошибки на примере приложения Django, которое предназначено для управления сотрудниками в различных организациях.

Базовый пример рассматриваемого кода

from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db import models

User = get_user_model()


class Organization(models.Model):
    name = models.CharField(max_length=100)
    datetime_created = models.DateTimeField(auto_now_add=True, editable=False)
    is_active = models.BooleanField(default=True)


class Employee(models.Model):
    user = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name="employees"
    )
    organization = models.ForeignKey(
        Organization, on_delete=models.CASCADE, related_name="employees"
    )
    is_currently_employed = models.BooleanField(default=True)
    reference_id = models.CharField(null=True, blank=True, max_length=255)
    last_clock_in = models.DateTimeField(null=True, blank=True)
    datetime_created = models.DateTimeField(auto_now_add=True, editable=False)

    def clean(self):
        try:
            if self.last_clock_in < self.datetime_created:
                raise ValidationError(
                    "Last clock in must occur after the employee entered"
                    " the system."
                )
        except TypeError:
            # Raises TypeError if there is no last_clock_in because
            # you cant compare None to datetime
            pass

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

for org in Organization.objects.filter(is_active=True):
    for emp in org.employees.all():
        if emp.is_currently_employed:
            do_something(org, emp)

Этот цикл приводит к генерации нового запроса для каждого сотрудника в нашей базе данных. Это может привести к десяткам тысяч запросов, что замедлит работу нашего приложения (Это так называемая проблема n + 1). Однако, если мы добавим prefetch_related к запросу Organization, мы минимизируем количество запросов.

for org in Organization.objects.filter(is_active=True).prefetch_related(
    "employees"
):

Добавление этих методов приводит к значительному повышению производительности без особых усилий, но о их добавлении можно легко забыть. Для ForeignKey или OneToOneField используйте select_related. Для обратного ForeignKey или ManyToManyField используйте prefetch_related. Мы могли бы сделать это более эффективным, начав с таблицы сотрудников и используя базу данных для фильтрации результатов. Нам все еще нужно добавить select_related, поскольку функция do_something использует организацию сотрудника. В противном случае цикл может привести к тысячам запросов к таблице организации.

for emp in Employee.objects.filter(
    organization__is_active=True, is_currently_employed=True
).select_related("organization"):
    do_something(emp.organization, emp)

Добавление null в CharField или TextField

В документации Django не рекомендуется добавлять null = True в CharField (Avoid using null on string-based fields such as CharField and TextField. ). В нашем примере кода ссылочный идентификатор сотрудника reference_id содержит null = True. В нашем примере приложения мы дополнительно интегрируемся с системой отслеживания сотрудников нашего клиента и используем reference_id в качестве идентификатора интегрированной системы.

reference_id = models.CharField(null=True, blank=True, max_length=255)

Добавление null = True означает, что поле имеет два значения «no data», null и пустую строку. Обычно Django использует пустую строку, чтобы не представлять данные. Имея также значение null в качестве значения «no data», мы можем вносить небольшие ошибки. Допустим, нам нужно написать код для получения данных из системы нашего клиента.

if employee.reference_id is not None:
    fetch_employee_record(employee)

В идеале вы пишете оператор if для обработки любых значений «no data», используя if employee.reference_id :, но я обнаружил, что на практике этого не происходит. Поскольку reference_id может быть либо нулевым, либо пустой строкой, мы создали здесь ошибку, при которой система пытается получить запись о сотруднике, если reference_id является пустой строкой. Ясно, что это не сработает и вызовет ошибку в нашей системе. Согласно документации Django, существует одно исключение для добавления null = True в CharField. Если вам нужно добавить как blank = True, так и unique = True в CharField, и соответственно потребуется null = True.

Сортировка по убыванию или по возрастанию с order_by или использование last

В Django order_by по умолчанию используется сортировка в возрастающем порядке. Добавляя символ “” перед ключевыми словами, вы говорите Django, что нужно указать порядок убывания. Давайте посмотрим на примере.

oldest_organization_first = Organization.objects.order_by("datetime_created")

newest_organization_first = Organization.objects.order_by("-datetime_created")

С минусом перед datetime_created, Django сначала дает нам новейшую организацию. И наоборот, без минуса, мы сначала получаем самую старую организацию. Непонимание работы сортировки по умолчанию могут привести к очень сложно обнаруживаемым ошибкам. Наборы запросов Django также поставляются с методам latest, который возвращает нам последний объект из таблице на основе переданных полей. Метод latest  по умолчанию работает в порядке убывания, в отличие от метода order_by, который по умолчанию использует сортировку в порядке возрастания.

oldest_organization_first = Organization.objects.latest("-datetime_created")

newest_organization_first = Organization.objects.latest("datetime_created")

В нескольких проектах я находил ошибки из-за разных типов сортировки по умолчанию в latest и order_by. Будьте осторожны при написании order_by и latest. Давайте посмотрим на эквивалентные запросы с использованием latest и order_by.

>>> oldest_org = Organization.objects.order_by("datetime_created")[:1][0]
>>> oldest_other_org = Organization.objects.latest("-datetime_created")
>>> oldest_org == oldest_other_org
True

>>> newest_org = Organization.objects.order_by("-datetime_created")[:1][0]
>>> newest_other_org = Organization.objects.latest("datetime_created")
>>> newest_org == newest_other_org
True

Метод clean не вызывается при сохранение модели

Согласно документации Django, методы проверки модели, такие как clean, validate_unique и clean_fields, не вызываются автоматически методом сохранения модели. В нашем примере кода модель сотрудника содержит метод clean, который говорит, что last_clock_in не должен иметь значение до того, как сотрудник не войдет в систему.

def clean(self):
    try:
        if self.last_clock_in < self.datetime_created:
            raise ValidationError(
                "Last clock in must occur after the employee entered"
                " the system."
            )
    except TypeError:
        # Raises TypeError if there is no last_clock_in because
        # you cant compare None to datetime
        pass

Допустим, у нас есть представление, которое обновляет время last_clock_in для нашего сотрудника, и как часть этого представления мы обновляем сотрудника, вызывая save.

from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.http import require_http_methods

from example_project.helpers import parse_request
from example_project.models import Employee


@require_http_methods(["POST"])
def update_employee_last_clock_in(request, employee_pk):
    clock_in_datetime = parse_request(request)
    employee = get_object_or_404(Employee, pk=employee_pk)
    employee.last_clock_in = clock_in_datetime
    employee.save()
    return HttpResponse(status=200)

В нашем примере представления мы вызываем save без вызова clean или full_clean, что означает, что clock_in_datetime, переданное в наше представление, будет иметь значение произойти до как получит значение datetime_created и при этом сохранится в базе данных. Это приводит к тому, что в нашу базу данных поступают недопустимые данные. Давайте исправим нашу ошибку.

employee.last_clock_in = clock_in_datetime
employee.full_clean()
employee.save()

Теперь, если clock_in_datetime предшествует datetime_created сотрудника, full_clean вызывает ValidationError, что предотвращает попадание недействительных данных в нашу базу данных.

Не использование update_fields при сохранении

Метод save Django Model использует аргумент update_fields. В типичной производственной установке Django люди используют gunicorn для запуска нескольких серверных процессов Django на одном компьютере и используют celery для запуска фоновых процессов. Когда вы вызываете save без update_fields, вся модель обновит все свои значения в памяти. Чтобы проиллюстрировать это, давайте посмотрим на пример используемого SQL при сохранение.

>>> user = User.objects.get(id=1)
>>> user.first_name = "Steven"
>>> user.save()
UPDATE "users_user"
   SET "password" = 'some_hash',
       "last_login" = '2021-02-25T22:43:41.033881+00:00'::timestamptz,
       "is_superuser" = false,
       "username" = 'stevenapate',
       "first_name" = 'Steven',
       "last_name" = '',
       "email" = 'steven@laac.dev',
       "is_staff" = false,
       "is_active" = true,
       "date_joined" = '2021-02-19T21:08:50.885795+00:00'::timestamptz,
 WHERE "users_user"."id" = 1
>>> user.first_name = "NotSteven"
>>> user.save(update_fields=["first_name"])
UPDATE "users_user"
   SET "first_name" = 'NotSteven'
 WHERE "users_user"."id" = 1

Первый вызов save без update_fields приводит к сохранению всех полей пользовательской модели. С update_fields обновляются только first_name. В производственной среде с частой вызовом save без update_fields может привести к состоянию гонки. Допустим, у нас запущены два процесса: gunicorn worker, работающий на нашем сервере Django, и celery worker. По заданному расписанию celery worker запрашивает внешнее API и обновляет is_active пользователя.

from celery import task
from django.contrib.auth import get_user_model

from example_project.external_api import get_user_status

User = get_user_model()


@task
def update_user_status(user_pk):
    user = User.objects.get(pk=user_pk)
    user_status = get_user_status(user)
    if user_status == "inactive":
        user.is_active = False
        user.save()

Celery worker запускает задачу, загружает весь пользовательский объект в память и запрашивает внешний API, но работа внешнего API занимает какое то время. Пока celery worker ожидает выполнение внешнего API, тот же пользователь подключается к нашему gunicorn worker и отправляет обновление на свой адрес электронной почты, изменяя его со steven@laac.dev на steven@stevenapate.com. После того, как обновление электронной почты записывается в базе данных, внешнее API отвечает, и celery worker обновляет is_active пользователя до False.

В этом случае celery worker перезаписывает обновление электронной почты, потому что gunicorn worker загрузил весь пользовательский объект в память до записи обновления электронной почты. Когда celery worker загрузил пользователя в память, его адрес электронной почты был steven@laac.dev. Этот адрес остается в памяти до тех пор, пока внешнее API не ответит и не перезапишет новый адрес электронной почты. В конце концов, строка, представляющая пользователя в базе данных, содержит старый адрес электронной почты steven@laac.dev и is_active = False. Нужно изменить код, чтобы предотвратить состояние гонки.

if user_status == "inactive":
    user.is_active = False
    user.save(update_fields=["is_active"])

Если предыдущий сценарий произошел с обновленным кодом, электронная почта пользователя останется steven@stevenapate.com после того, как celery worker обновит is_active, потому что обновление записывает только поле is_active. Только в редких случаях, например при создании нового объекта, можно вызывать save без update_fields. Для получения дополнительной информации по этой теме я рекомендую этот тикет в трекере ошибок Django и этот запрос на перенос для Django REST Framework.

Примечание

Хотя вы можете решить эту проблему в своей базе кода, не вызывая простой метод сохранения, сторонние пакеты Django могут так же содержать эту проблему. Например, Django REST Framework не использует update_fields в запросах PATCH. Django REST Framework – фантастический пакет, который я люблю использовать, но в нем етсь такая проблема. Помните об этом при добавлении сторонних пакетов в ваш проект Django.

Заключение

Я сам допускал все эти ошибки, много раз. Я надеюсь, что этот пост поможет вам их больше не совершать. Мне нравится использовать Django, и я считаю, что это отличный фреймворк для создания веб-приложений.

Была ли вам полезна эта статья?
[8 / 4.8]

Spread the love
Подписаться
Уведомление о
guest
2 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Павел
Павел
3 лет назад

Спасибо! Пишите еще. Опыт очень интересный и полезный.

Анонимно
Анонимно
2 лет назад

Круто спасибо что пишите и переводите