Ошибка 500 сервера при использовании Django Rest Framework

Я использую Django/DRF с Djoser и djangorestframework-simplejwt для создания API для полной аутентификации, включая регистрацию, вход, активацию, забытый пароль и сброс пароля.

Я следовал этому учебнику YT

По какой-то причине, когда я отправляю POST-запрос в Postman на localhost:8000/api/users/, я получаю эту ошибку, и я понятия не имею, почему на данный момент, django.db.utils.DatabaseError: Save with update_fields did not affect any rows.

Я использую не SQLite, а настоящую базу данных Postgres на localhost. Я пробовал заменить user.save(self._db) на просто user.save(), та же ошибка. Я обновил Django до 5x. Django - единственное значительное обновление по сравнению с учебником, он использует Django 4x.

Я перенес часть оригинального кода модели в файл managers.py, основываясь на этом руководстве testdriven.io

Я смог запустить python manage.py runserver без ошибок после этого.

Похоже, что это не код, связанный с учебником, а что-то с пакетами python...

Вот ошибка из cli:

My users/models.py:

from django.contrib.postgres.functions import RandomUUID
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import (
    AbstractBaseUser,
    PermissionsMixin
)

from .managers import UserAccountManager
from django.utils.translation import gettext_lazy as _


class UserAccount(AbstractBaseUser, PermissionsMixin):
    user_id = models.UUIDField(primary_key=True, default=RandomUUID, editable=False)
    first_name = models.CharField(_('first_name'), max_length=255)
    last_name = models.CharField(_('last_name'), max_length=255)
    email = models.EmailField(unique=True, max_length=255)
    title = models.CharField(_('title'), max_length=55, blank=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    is_superuser = models.BooleanField(default=False)
    date_joined = models.DateTimeField(default=timezone.now)

    objects = UserAccountManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['first_name', 'last_name']

    def __str__(self):
        return self.email

My users/managers.py:

from django.contrib.auth.models import BaseUserManager
from django.utils.translation import gettext_lazy as _
from django.db import models


class UserAccountManager(BaseUserManager):
    def create_user(self, email, password=None, **kwargs):
        """
        Creates and saves a User with the given email, date of
        birth and password.
        """
        if not email:
            raise ValueError(_('Users must have an email address'))

        email = self.normalize_email(email)
        email = email.lower()

        user = self.model(
            email=email,
            **kwargs
        )

        user.set_password(password)
        user.save()
        return user

    def create_superuser(self, email, password=None, **kwargs):
        """
        Creates and saves a superuser with the given email, date of
        birth and password.
        """
        user = self.create_user(
            email,
            password=password,
            **kwargs
        )

        user.is_staff = True
        user.is_superuser = True
        user.save()
        return user

В учебнике рассказывается о том, как использовать HTTP-ONLY cookies для управления токенами, мы помещаем этот код в authentication.py

А это users/authentication.py:

from django.conf import settings
from rest_framework_simplejwt.authentication import JWTAuthentication


    class CustomJWTAuthentication(JWTAuthentication):
        def authenticate(self, request):
            try:
                header = self.get_header(request)
    
                if header is None:
                    raw_token = request.COOKIES.get(settings.AUTH_COOKIE)
                else:
                    raw_token = self.get_raw_token(header)
    
                if raw_token is None:
                    return None
    
                validated_token = self.get_validated_token(raw_token)
    
                return self.get_user(validated_token), validated_token
            except:
                return None

Если кому-то нужен дополнительный код, просто дайте мне знать. Опять же, я не уверен, почему я получаю эту 500 ошибку сервера, которая не позволяет сохранить данные в базе данных?

Как я понимаю, это вызвано взаимодействием между одной из ваших зависимостей, djoser, и уровнем изоляции, который вы установили в вашей базе данных.

Для начала объясним, с чем связана ошибка «Save with update_fields did not affect any rows.». Эта ошибка означает, что Django попытался обновить строку базы данных в соответствии с состоянием объекта ORM. Часть update_fields действует как оптимизация производительности: она предполагает, что строка базы данных в основном создана, и выдает только операторы UPDATE ... SET ..., соответствующие значениям, указанным в update_fields. В данном случае он пытается установить is_active. Однако, когда он пытается установить это поле, то обнаруживает, что нулевая строка соответствует первичному ключу только что созданного объекта.

Почему он пытается обновить is_active? Вот блок кода, который объясняет это:

class UserCreateMixin:
    # ...
    def perform_create(self, validated_data):
        with transaction.atomic():
            user = User.objects.create_user(**validated_data)
            if settings.SEND_ACTIVATION_EMAIL:
                user.is_active = False
                user.save(update_fields=["is_active"])
        return user

https://github.com/sunscrapers/djoser/blob/2.3.1/djoser/serializers.py#L46

При создании пользователя выполняются следующие действия:

  1. Начните транзакцию. Это гарантирует, что другие программы не увидят частично созданного пользователя.
  2. Создаем пользователя, используя данные из сериализатора.
  3. Если у нас включены активационные письма, пометьте пользователя как неактивного. (Вот почему требуется транзакция - мы не хотим создавать пользователя, которому разрешено входить в систему, а затем удалять его возможность входить в систему. Это позволит пользователям обходить проверку электронной почты в течение короткого периода времени.)
  4. Сохраните поле is_active в базе данных.
  5. Закройте транзакцию, зафиксировав сразу обе записи.

Это дает нам Обходной путь №1: если вы отключите settings.SEND_ACTIVATION_EMAIL, проблемная строка никогда не будет выполняться, и поэтому вы никогда не получите эту ошибку.

Этот код неявно предполагает, что база данных позволяет читать собственные записи внутри транзакции.

Что Postgres делает по умолчанию?

Read Committed - это уровень изоляции по умолчанию в PostgreSQL. Когда транзакция использует этот уровень изоляции, запрос SELECT (без предложения FOR UPDATE/SHARE) видит только данные, зафиксированные до начала выполнения запроса; он никогда не видит ни незафиксированных данных, ни изменений, зафиксированных параллельными транзакциями во время выполнения запроса.

https://www.postgresql.org/docs/current/transaction-iso.html#XACT-READ-COMMITTED

Поскольку создание пользователя происходит во время транзакции, оно не видно последующему запросу, который проверяет, существует ли объект.

Это дает нам обходной путь №2: если вы измените уровень изоляции Postgres на SERIALIZABLE, то чтения внутри транзакции смогут видеть свои записи. Вы можете прочитать, как установить этот уровень здесь, и вам, вероятно, следует прочитать документацию postgres о SERIALIZABLE, поскольку эта настройка имеет некоторые существенные последствия для производительности.

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

Например, UserCreateMixin.perform_create() можно изменить так, чтобы он больше не требовал транзакций. Это кажется наиболее простым решением.

Предупреждение: этот код не протестирован.

    def perform_create(self, validated_data):
        if settings.SEND_ACTIVATION_EMAIL:
            validated_data['is_active'] = False
        user = User.objects.create_user(**validated_data)
        return user

Вы, вероятно, захотите прочитать их руководство по внесению вклада, чтобы узнать, как установить djoser так, чтобы его можно было модифицировать.

Вернуться на верх