Выбор полей (сhoices) Django в действительности не ограничивает ваши данные

Spread the love

Перевод: Adam JohnsonDjango’s Field Choices Don’t Constrain Your Data

Этот статья описывает несколько не очевидный способ работы Field.choices в Django.

Возьмем для примера определение модели Django:

from django.db import models

class Status(models.TextChoices):
    UNPUBLISHED = 'UN', 'Unpublished'
    PUBLISHED = 'PB', 'Published'


class Book(models.Model):
    status = models.CharField(
        max_length=2,
        choices=Status.choices,
        default=Status.UNPUBLISHED,
    )

    def __str__(self):
        return f"{self.id} - {Status(self.status).label}"

Если мы откроем shell, мы можем легко создать Book с заданным выбором status:

$ python manage.py shell  # with ipython installed
...
In [1]: from core.models import Status, Book

In [2]: Book.objects.create(status=Status.UNPUBLISHED)
Out[2]: <Book: 1 - Unpublished>

Список вариантов (choices) ограничивает значение status во время проверки модели (model validation) в Python:

In [3]: book = Book.objects.get(id=1)

In [4]: book.status = 'republished'

In [5]: book.full_clean()
---------------------------------------------------------------------------
ValidationError                           Traceback (most recent call last)
<ipython-input-7-e64237e0a92a> in <module>
----> 1 book.full_clean()

.../django/db/models/base.py in full_clean(self, exclude, validate_unique)
   1220
   1221         if errors:
-> 1222             raise ValidationError(errors)
   1223
   1224     def clean_fields(self, exclude=None):

ValidationError: {'status': ["Value 'republished' is not a valid choice."]}

И это отлично подходит для ModelForms и других случаев с использованием проверки. Пользователи не могут выбирать неправильные варианты и получать сообщения об ошибки если попробуют записать неверные данные.

К сожалению, нам, как разработчикам, по-прежнему легко записать эти недопустимые данные в базу данных:

In [6]: book.save()

Опс!

Также возможно обновить все наши экземпляры до недопустимого значения status через однострочную команду:

In [8]: Book.objects.update(status='republished')
Out[8]: 1

Итак, что происходит? Почему Django позволяет нам объявлять набор вариантов, которые мы хотим, чтобы поле принимало, а затем позволяет нам легко обойти это?

Что ж, проверка модели в Django предназначена в основном для форм. Он полагается, что другой код в вашем приложении «знают, что делает».

Если мы хотим предотвратить это, самое общее решение – заставить саму базу данных отклонять неверные данные. Это не только сделает ваш код Django более надежным, но и любые другие приложения, использующие базу данных, также будут использовать ограничения.

Мы можем добавить такие ограничения с помощью класса CheckConstraint, добавленного в Django 2.2. Для нашей модели нам нужно определить и назвать единственный фильтр CheckConstraint в Meta.constraints:

class Book(models.Model):
    status = models.CharField(
        max_length=2,
        choices=Status.choices,
        default=Status.UNPUBLISHED,
    )

    def __str__(self):
        return f"{self.id} - {Status(self.status).label}"

    class Meta:
        constraints = [
            models.CheckConstraint(
                name="%(app_label)s_%(class)s_status_valid",
                check=models.Q(status__in=Status.values),
            )
        ]

Объект Q представляет собой одно выражение, которое мы передаем в Model.objects.filter (). Ограничения могут иметь любое количество логики для полей в текущей модели. Это включает в себя все виды поиска, сравнения между полями и функции базы данных.

Запустив makemigrations, мы получим миграцию, которая выглядит так:

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ("core", "0001_initial"),
    ]

    operations = [
        migrations.AddConstraint(
            model_name="book",
            constraint=models.CheckConstraint(
                check=models.Q(status__in=["UN", "PB"]),
                name="%(app_label)s_%(class)s_status_valid",
            ),
        ),
    ]

Если мы попытаемся применить ее, пока база данных содержит недопустимые данные, то мы получим ошибку:

$ python manage.py migrate
Operations to perform:
  Apply all migrations: core
Running migrations:
  Applying core.0002_book_status_valid...Traceback (most recent call last):
...
  File "/.../django/db/backends/sqlite3/base.py", line 396, in execute
    return Database.Cursor.execute(self, query, params)
django.db.utils.IntegrityError: CHECK constraint failed: status_valid

Если мы очистим эти данные вручную и попробуем снова, они пройдут:

$ python manage.py migrate
Operations to perform:
  Apply all migrations: core
Running migrations:
  Applying core.0002_book_status_valid... OK

С этого момента база данных не позволит нам вставлять недопустимые строки или обновлять допустимые строки, чтобы они были недопустимыми:

In [4]: book.save()
---------------------------------------------------------------------------
...
/.../django/db/backends/sqlite3/base.py in execute(self, query, params)
    394             return Database.Cursor.execute(self, query)
    395         query = self.convert_query(query)
--> 396         return Database.Cursor.execute(self, query, params)
    397
    398     def executemany(self, query, param_list):

IntegrityError: CHECK constraint failed: status_valid

In [5]: Book.objects.update(status='republished')
---------------------------------------------------------------------------
...
/.../django/db/backends/sqlite3/base.py in execute(self, query, params)
    394             return Database.Cursor.execute(self, query)
    395         query = self.convert_query(query)
--> 396         return Database.Cursor.execute(self, query, params)
    397
    398     def executemany(self, query, param_list):

IntegrityError: CHECK constraint failed: status_valid

Супер!

В настоящее время Django не может отображать ошибки IntegrityErrors для пользователей при проверке модели. Ничто не сможет их уловить и превратить в ValidationErrors, которые могут нести сообщения, обращенные к пользователю. Согласно документации:

В общем случае ограничения не проверяются во время full_clean () и не вызывают ValidationErrors.

Чтобы это исправить, есть open ticket #30581.

В нашем случае, поскольку мы все еще используем choices, это нормально. Проверка сама по себе не позволит пользователям выбирать недопустимые статусы.

Для более сложных ограничений можно продублировать логику в Python с помощью custom validator.

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

Spread the love
Подписаться
Уведомление о
guest
0 Комментарий
Inline Feedbacks
View all comments