Пять распространенных ошибок Django
Перевод: Steven Pate — Five 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
Не использование select_related и prefetch_related
Допустим, мы пишем код, который перебирает всех активных сотрудников организации.
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, и я считаю, что это отличный фреймворк для создания веб-приложений.
Спасибо! Пишите еще. Опыт очень интересный и полезный.
Круто спасибо что пишите и переводите